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/history/starred_url_database.h"
6
7#include "app/sql/statement.h"
8#include "base/file_util.h"
9#include "base/json/json_writer.h"
10#include "base/logging.h"
11#include "base/memory/scoped_vector.h"
12#include "base/stl_util-inl.h"
13#include "base/string_util.h"
14#include "base/utf_string_conversions.h"
15#include "base/values.h"
16#include "chrome/browser/bookmarks/bookmark_codec.h"
17#include "chrome/browser/bookmarks/bookmark_model.h"
18#include "chrome/browser/history/history.h"
19#include "chrome/browser/history/query_parser.h"
20
21// The following table is used to store star (aka bookmark) information. This
22// class derives from URLDatabase, which has its own schema.
23//
24// starred
25//   id                 Unique identifier (primary key) for the entry.
26//   type               Type of entry, if 0 this corresponds to a URL, 1 for
27//                      a system folder, 2 for a user created folder, 3 for
28//                      other.
29//   url_id             ID of the url, only valid if type == 0
30//   group_id           ID of the folder, only valid if type != 0. This id comes
31//                      from the UI and is NOT the same as id.
32//   title              User assigned title.
33//   date_added         Creation date.
34//   visual_order       Visual order within parent.
35//   parent_id          Folder ID of the parent this entry is contained in, if 0
36//                      entry is not in a folder.
37//   date_modified      Time the folder was last modified. See comments in
38//                      StarredEntry::date_folder_modified
39// NOTE: group_id and parent_id come from the UI, id is assigned by the
40// db.
41
42namespace history {
43
44namespace {
45
46// Fields used by FillInStarredEntry.
47#define STAR_FIELDS \
48    " starred.id, starred.type, starred.title, starred.date_added, " \
49    "starred.visual_order, starred.parent_id, urls.url, urls.id, " \
50    "starred.group_id, starred.date_modified "
51const char kHistoryStarFields[] = STAR_FIELDS;
52
53void FillInStarredEntry(const sql::Statement& s, StarredEntry* entry) {
54  DCHECK(entry);
55  entry->id = s.ColumnInt64(0);
56  switch (s.ColumnInt(1)) {
57    case 0:
58      entry->type = history::StarredEntry::URL;
59      entry->url = GURL(s.ColumnString(6));
60      break;
61    case 1:
62      entry->type = history::StarredEntry::BOOKMARK_BAR;
63      break;
64    case 2:
65      entry->type = history::StarredEntry::USER_FOLDER;
66      break;
67    case 3:
68      entry->type = history::StarredEntry::OTHER;
69      break;
70    default:
71      NOTREACHED();
72      break;
73  }
74  entry->title = s.ColumnString16(2);
75  entry->date_added = base::Time::FromInternalValue(s.ColumnInt64(3));
76  entry->visual_order = s.ColumnInt(4);
77  entry->parent_folder_id = s.ColumnInt64(5);
78  entry->url_id = s.ColumnInt64(7);
79  entry->folder_id = s.ColumnInt64(8);
80  entry->date_folder_modified = base::Time::FromInternalValue(s.ColumnInt64(9));
81}
82
83}  // namespace
84
85StarredURLDatabase::StarredURLDatabase() {
86}
87
88StarredURLDatabase::~StarredURLDatabase() {
89}
90
91bool StarredURLDatabase::MigrateBookmarksToFile(const FilePath& path) {
92  if (!GetDB().DoesTableExist("starred"))
93    return true;
94
95  if (EnsureStarredIntegrity() && !MigrateBookmarksToFileImpl(path)) {
96    NOTREACHED() << " Bookmarks migration failed";
97    return false;
98  }
99
100  if (!GetDB().Execute("DROP TABLE starred")) {
101    NOTREACHED() << "Unable to drop starred table";
102    return false;
103  }
104  return true;
105}
106
107bool StarredURLDatabase::GetAllStarredEntries(
108    std::vector<StarredEntry>* entries) {
109  DCHECK(entries);
110  std::string sql = "SELECT ";
111  sql.append(kHistoryStarFields);
112  sql.append("FROM starred LEFT JOIN urls ON starred.url_id = urls.id ");
113  sql += "ORDER BY parent_id, visual_order";
114
115  sql::Statement s(GetDB().GetUniqueStatement(sql.c_str()));
116  if (!s) {
117    NOTREACHED() << "Statement prepare failed";
118    return false;
119  }
120
121  history::StarredEntry entry;
122  while (s.Step()) {
123    FillInStarredEntry(s, &entry);
124    // Reset the url for non-url types. This is needed as we're reusing the
125    // same entry for the loop.
126    if (entry.type != history::StarredEntry::URL)
127      entry.url = GURL();
128    entries->push_back(entry);
129  }
130  return true;
131}
132
133bool StarredURLDatabase::EnsureStarredIntegrity() {
134  std::set<StarredNode*> roots;
135  std::set<StarID> folders_with_duplicate_ids;
136  std::set<StarredNode*> unparented_urls;
137  std::set<StarID> empty_url_ids;
138
139  if (!BuildStarNodes(&roots, &folders_with_duplicate_ids, &unparented_urls,
140                      &empty_url_ids)) {
141    return false;
142  }
143
144  bool valid = EnsureStarredIntegrityImpl(&roots, folders_with_duplicate_ids,
145                                          &unparented_urls, empty_url_ids);
146
147  STLDeleteElements(&roots);
148  STLDeleteElements(&unparented_urls);
149  return valid;
150}
151
152bool StarredURLDatabase::UpdateStarredEntryRow(StarID star_id,
153                                               const string16& title,
154                                               UIStarID parent_folder_id,
155                                               int visual_order,
156                                               base::Time date_modified) {
157  DCHECK(star_id && visual_order >= 0);
158  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
159      "UPDATE starred SET title=?, parent_id=?, visual_order=?, "
160      "date_modified=? WHERE id=?"));
161  if (!statement)
162    return 0;
163
164  statement.BindString16(0, title);
165  statement.BindInt64(1, parent_folder_id);
166  statement.BindInt(2, visual_order);
167  statement.BindInt64(3, date_modified.ToInternalValue());
168  statement.BindInt64(4, star_id);
169  return statement.Run();
170}
171
172bool StarredURLDatabase::AdjustStarredVisualOrder(UIStarID parent_folder_id,
173                                                  int start_visual_order,
174                                                  int delta) {
175  DCHECK(parent_folder_id && start_visual_order >= 0);
176  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
177      "UPDATE starred SET visual_order=visual_order+? "
178      "WHERE parent_id=? AND visual_order >= ?"));
179  if (!statement)
180    return false;
181
182  statement.BindInt(0, delta);
183  statement.BindInt64(1, parent_folder_id);
184  statement.BindInt(2, start_visual_order);
185  return statement.Run();
186}
187
188StarID StarredURLDatabase::CreateStarredEntryRow(URLID url_id,
189                                                 UIStarID folder_id,
190                                                 UIStarID parent_folder_id,
191                                                 const string16& title,
192                                                 const base::Time& date_added,
193                                                 int visual_order,
194                                                 StarredEntry::Type type) {
195  DCHECK(visual_order >= 0 &&
196         (type != history::StarredEntry::URL || url_id));
197  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
198      "INSERT INTO starred "
199      "(type, url_id, group_id, title, date_added, visual_order, parent_id, "
200      "date_modified) VALUES (?,?,?,?,?,?,?,?)"));
201  if (!statement)
202    return 0;
203
204  switch (type) {
205    case history::StarredEntry::URL:
206      statement.BindInt(0, 0);
207      break;
208    case history::StarredEntry::BOOKMARK_BAR:
209      statement.BindInt(0, 1);
210      break;
211    case history::StarredEntry::USER_FOLDER:
212      statement.BindInt(0, 2);
213      break;
214    case history::StarredEntry::OTHER:
215      statement.BindInt(0, 3);
216      break;
217    default:
218      NOTREACHED();
219  }
220  statement.BindInt64(1, url_id);
221  statement.BindInt64(2, folder_id);
222  statement.BindString16(3, title);
223  statement.BindInt64(4, date_added.ToInternalValue());
224  statement.BindInt(5, visual_order);
225  statement.BindInt64(6, parent_folder_id);
226  statement.BindInt64(7, base::Time().ToInternalValue());
227  if (statement.Run())
228    return GetDB().GetLastInsertRowId();
229  return 0;
230}
231
232bool StarredURLDatabase::DeleteStarredEntryRow(StarID star_id) {
233  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
234      "DELETE FROM starred WHERE id=?"));
235  if (!statement)
236    return false;
237
238  statement.BindInt64(0, star_id);
239  return statement.Run();
240}
241
242bool StarredURLDatabase::GetStarredEntry(StarID star_id, StarredEntry* entry) {
243  DCHECK(entry && star_id);
244  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
245      "SELECT" STAR_FIELDS "FROM starred LEFT JOIN urls ON "
246      "starred.url_id = urls.id WHERE starred.id=?"));
247  if (!statement)
248    return false;
249
250  statement.BindInt64(0, star_id);
251
252  if (statement.Step()) {
253    FillInStarredEntry(statement, entry);
254    return true;
255  }
256  return false;
257}
258
259StarID StarredURLDatabase::CreateStarredEntry(StarredEntry* entry) {
260  entry->id = 0;  // Ensure 0 for failure case.
261
262  // Adjust the visual order when we are inserting it somewhere.
263  if (entry->parent_folder_id)
264    AdjustStarredVisualOrder(entry->parent_folder_id, entry->visual_order, 1);
265
266  // Insert the new entry.
267  switch (entry->type) {
268    case StarredEntry::USER_FOLDER:
269      entry->id = CreateStarredEntryRow(0, entry->folder_id,
270          entry->parent_folder_id, entry->title, entry->date_added,
271          entry->visual_order, entry->type);
272      break;
273
274    case StarredEntry::URL: {
275      // Get the row for this URL.
276      URLRow url_row;
277      if (!GetRowForURL(entry->url, &url_row)) {
278        // Create a new URL row for this entry.
279        url_row = URLRow(entry->url);
280        url_row.set_title(entry->title);
281        url_row.set_hidden(false);
282        entry->url_id = this->AddURL(url_row);
283      } else {
284        entry->url_id = url_row.id();  // The caller doesn't have to set this.
285      }
286
287      // Create the star entry referring to the URL row.
288      entry->id = CreateStarredEntryRow(entry->url_id, entry->folder_id,
289          entry->parent_folder_id, entry->title, entry->date_added,
290          entry->visual_order, entry->type);
291
292      // Update the URL row to refer to this new starred entry.
293      UpdateURLRow(entry->url_id, url_row);
294      break;
295    }
296
297    default:
298      NOTREACHED();
299      break;
300  }
301  return entry->id;
302}
303
304UIStarID StarredURLDatabase::GetMaxFolderID() {
305  sql::Statement max_folder_id_statement(GetDB().GetUniqueStatement(
306      "SELECT MAX(group_id) FROM starred"));
307  if (!max_folder_id_statement) {
308    NOTREACHED() << GetDB().GetErrorMessage();
309    return 0;
310  }
311  if (!max_folder_id_statement.Step()) {
312    NOTREACHED() << GetDB().GetErrorMessage();
313    return 0;
314  }
315  return max_folder_id_statement.ColumnInt64(0);
316}
317
318bool StarredURLDatabase::BuildStarNodes(
319    std::set<StarredURLDatabase::StarredNode*>* roots,
320    std::set<StarID>* folders_with_duplicate_ids,
321    std::set<StarredNode*>* unparented_urls,
322    std::set<StarID>* empty_url_ids) {
323  std::vector<StarredEntry> star_entries;
324  if (!GetAllStarredEntries(&star_entries)) {
325    NOTREACHED() << "Unable to get bookmarks from database";
326    return false;
327  }
328
329  // Create the folder/bookmark-bar/other nodes.
330  std::map<UIStarID, StarredNode*> folder_id_to_node_map;
331  for (size_t i = 0; i < star_entries.size(); ++i) {
332    if (star_entries[i].type != StarredEntry::URL) {
333      if (folder_id_to_node_map.find(star_entries[i].folder_id) !=
334          folder_id_to_node_map.end()) {
335        // There's already a folder with this ID.
336        folders_with_duplicate_ids->insert(star_entries[i].id);
337      } else {
338        // Create the node and update the mapping.
339        StarredNode* node = new StarredNode(star_entries[i]);
340        folder_id_to_node_map[star_entries[i].folder_id] = node;
341      }
342    }
343  }
344
345  // Iterate again, creating nodes for URL bookmarks and parenting all
346  // bookmarks/folders. In addition populate the empty_url_ids with all entries
347  // of type URL that have an empty URL.
348  std::map<StarID, StarredNode*> id_to_node_map;
349  for (size_t i = 0; i < star_entries.size(); ++i) {
350    if (star_entries[i].type == StarredEntry::URL) {
351      if (star_entries[i].url.is_empty()) {
352        empty_url_ids->insert(star_entries[i].id);
353      } else if (!star_entries[i].parent_folder_id ||
354          folder_id_to_node_map.find(star_entries[i].parent_folder_id) ==
355          folder_id_to_node_map.end()) {
356        // This entry has no parent, or we couldn't find the parent.
357        StarredNode* node = new StarredNode(star_entries[i]);
358        unparented_urls->insert(node);
359      } else {
360        // Add the node to its parent.
361        StarredNode* parent =
362            folder_id_to_node_map[star_entries[i].parent_folder_id];
363        StarredNode* node = new StarredNode(star_entries[i]);
364        parent->Add(node, parent->child_count());
365      }
366    } else if (folders_with_duplicate_ids->find(star_entries[i].id) ==
367               folders_with_duplicate_ids->end()) {
368      // The entry is a folder (or bookmark bar/other node) that isn't
369      // marked as a duplicate.
370      if (!star_entries[i].parent_folder_id ||
371          folder_id_to_node_map.find(star_entries[i].parent_folder_id) ==
372          folder_id_to_node_map.end()) {
373        // Entry has no parent, or the parent wasn't found.
374        roots->insert(folder_id_to_node_map[star_entries[i].folder_id]);
375      } else {
376        // Parent the folder node.
377        StarredNode* parent =
378            folder_id_to_node_map[star_entries[i].parent_folder_id];
379        StarredNode* node = folder_id_to_node_map[star_entries[i].folder_id];
380        if (!node->HasAncestor(parent) && !parent->HasAncestor(node)) {
381          parent->Add(node, parent->child_count());
382        } else {
383          // The node has a cycle. Add it to the list of roots so the cycle is
384          // broken.
385          roots->insert(node);
386        }
387      }
388    }
389  }
390  return true;
391}
392
393StarredURLDatabase::StarredNode* StarredURLDatabase::GetNodeByType(
394    const std::set<StarredURLDatabase::StarredNode*>& nodes,
395    StarredEntry::Type type) {
396  for (std::set<StarredNode*>::const_iterator i = nodes.begin();
397       i != nodes.end(); ++i) {
398    if ((*i)->value.type == type)
399      return *i;
400  }
401  return NULL;
402}
403
404bool StarredURLDatabase::EnsureVisualOrder(
405    StarredURLDatabase::StarredNode* node) {
406  for (int i = 0; i < node->child_count(); ++i) {
407    if (node->GetChild(i)->value.visual_order != i) {
408      StarredEntry& entry = node->GetChild(i)->value;
409      entry.visual_order = i;
410      LOG(WARNING) << "Bookmark visual order is wrong";
411      if (!UpdateStarredEntryRow(entry.id, entry.title, entry.parent_folder_id,
412                                 i, entry.date_folder_modified)) {
413        NOTREACHED() << "Unable to update visual order";
414        return false;
415      }
416    }
417    if (!EnsureVisualOrder(node->GetChild(i)))
418      return false;
419  }
420  return true;
421}
422
423bool StarredURLDatabase::EnsureStarredIntegrityImpl(
424    std::set<StarredURLDatabase::StarredNode*>* roots,
425    const std::set<StarID>& folders_with_duplicate_ids,
426    std::set<StarredNode*>* unparented_urls,
427    const std::set<StarID>& empty_url_ids) {
428  // Make sure the bookmark bar entry exists.
429  StarredNode* bookmark_node =
430      GetNodeByType(*roots, StarredEntry::BOOKMARK_BAR);
431  if (!bookmark_node) {
432    LOG(WARNING) << "No bookmark bar folder in database";
433    // If there is no bookmark bar entry in the db things are really
434    // screwed. Return false, which won't trigger migration and we'll just
435    // drop the tables.
436    return false;
437  }
438
439  // Make sure the other node exists.
440  StarredNode* other_node = GetNodeByType(*roots, StarredEntry::OTHER);
441  if (!other_node) {
442    LOG(WARNING) << "No bookmark other folder in database";
443    StarredEntry entry;
444    entry.folder_id = GetMaxFolderID() + 1;
445    if (entry.folder_id == 1) {
446      NOTREACHED() << "Unable to get new id for other bookmarks folder";
447      return false;
448    }
449    entry.id = CreateStarredEntryRow(
450        0, entry.folder_id, 0, UTF8ToUTF16("other"), base::Time::Now(), 0,
451        history::StarredEntry::OTHER);
452    if (!entry.id) {
453      NOTREACHED() << "Unable to create other bookmarks folder";
454      return false;
455    }
456    entry.type = StarredEntry::OTHER;
457    StarredNode* other_node = new StarredNode(entry);
458    roots->insert(other_node);
459  }
460
461  // We could potentially make sure only one folder with type
462  // BOOKMARK_BAR/OTHER, but history backend enforces this.
463
464  // Nuke any entries with no url.
465  for (std::set<StarID>::const_iterator i = empty_url_ids.begin();
466       i != empty_url_ids.end(); ++i) {
467    LOG(WARNING) << "Bookmark exists with no URL";
468    if (!DeleteStarredEntryRow(*i)) {
469      NOTREACHED() << "Unable to delete bookmark";
470      return false;
471    }
472  }
473
474  // Make sure the visual order of the nodes is correct.
475  for (std::set<StarredNode*>::const_iterator i = roots->begin();
476       i != roots->end(); ++i) {
477    if (!EnsureVisualOrder(*i))
478      return false;
479  }
480
481  // Move any unparented bookmarks to the bookmark bar.
482  {
483    std::set<StarredNode*>::iterator i = unparented_urls->begin();
484    while (i != unparented_urls->end()) {
485      LOG(WARNING) << "Bookmark not in a bookmark folder found";
486      if (!Move(*i, bookmark_node))
487        return false;
488      unparented_urls->erase(i++);
489    }
490  }
491
492  // Nuke any folders with duplicate ids. A duplicate id means there are two
493  // folders in the starred table with the same folder_id. We only keep the
494  // first folder, all other folders are removed.
495  for (std::set<StarID>::const_iterator i = folders_with_duplicate_ids.begin();
496       i != folders_with_duplicate_ids.end(); ++i) {
497    LOG(WARNING) << "Duplicate folder id in bookmark database";
498    if (!DeleteStarredEntryRow(*i)) {
499      NOTREACHED() << "Unable to delete folder";
500      return false;
501    }
502  }
503
504  // Move unparented user folders back to the bookmark bar.
505  {
506    std::set<StarredNode*>::iterator i = roots->begin();
507    while (i != roots->end()) {
508      if ((*i)->value.type == StarredEntry::USER_FOLDER) {
509        LOG(WARNING) << "Bookmark folder not on bookmark bar found";
510        if (!Move(*i, bookmark_node))
511          return false;
512        roots->erase(i++);
513      } else {
514        ++i;
515      }
516    }
517  }
518
519  return true;
520}
521
522bool StarredURLDatabase::Move(StarredNode* source, StarredNode* new_parent) {
523  history::StarredEntry& entry = source->value;
524  entry.visual_order = new_parent->child_count();
525  entry.parent_folder_id = new_parent->value.folder_id;
526  if (!UpdateStarredEntryRow(entry.id, entry.title,
527                             entry.parent_folder_id, entry.visual_order,
528                             entry.date_folder_modified)) {
529    NOTREACHED() << "Unable to move folder";
530    return false;
531  }
532  new_parent->Add(source, new_parent->child_count());
533  return true;
534}
535
536bool StarredURLDatabase::MigrateBookmarksToFileImpl(const FilePath& path) {
537  std::vector<history::StarredEntry> entries;
538  if (!GetAllStarredEntries(&entries))
539    return false;
540
541  // Create the bookmark bar and other folder nodes.
542  history::StarredEntry entry;
543  entry.type = history::StarredEntry::BOOKMARK_BAR;
544  BookmarkNode bookmark_bar_node(0, GURL());
545  bookmark_bar_node.Reset(entry);
546  entry.type = history::StarredEntry::OTHER;
547  BookmarkNode other_node(0, GURL());
548  other_node.Reset(entry);
549
550  std::map<history::UIStarID, history::StarID> folder_id_to_id_map;
551  typedef std::map<history::StarID, BookmarkNode*> IDToNodeMap;
552  IDToNodeMap id_to_node_map;
553
554  history::UIStarID other_folder_folder_id = 0;
555  history::StarID other_folder_id = 0;
556
557  // Iterate through the entries building a mapping between folder_id and id.
558  for (std::vector<history::StarredEntry>::const_iterator i = entries.begin();
559       i != entries.end(); ++i) {
560    if (i->type != history::StarredEntry::URL) {
561      folder_id_to_id_map[i->folder_id] = i->id;
562      if (i->type == history::StarredEntry::OTHER) {
563        other_folder_id = i->id;
564        other_folder_folder_id = i->folder_id;
565      }
566    }
567  }
568
569  // Register the bookmark bar and other folder nodes in the maps.
570  id_to_node_map[HistoryService::kBookmarkBarID] = &bookmark_bar_node;
571  folder_id_to_id_map[HistoryService::kBookmarkBarID] =
572      HistoryService::kBookmarkBarID;
573  if (other_folder_folder_id) {
574    id_to_node_map[other_folder_id] = &other_node;
575    folder_id_to_id_map[other_folder_folder_id] = other_folder_id;
576  }
577
578  // Iterate through the entries again creating the nodes.
579  for (std::vector<history::StarredEntry>::iterator i = entries.begin();
580       i != entries.end(); ++i) {
581    if (!i->parent_folder_id) {
582      DCHECK(i->type == history::StarredEntry::BOOKMARK_BAR ||
583             i->type == history::StarredEntry::OTHER);
584      // Only entries with no parent should be the bookmark bar and other
585      // bookmarks folders.
586      continue;
587    }
588
589    BookmarkNode* node = id_to_node_map[i->id];
590    if (!node) {
591      // Creating a node results in creating the parent. As such, it is
592      // possible for the node representing a folder to have been created before
593      // encountering the details.
594
595      // The created nodes are owned by the root node.
596      node = new BookmarkNode(0, i->url);
597      id_to_node_map[i->id] = node;
598    }
599    node->Reset(*i);
600
601    DCHECK(folder_id_to_id_map.find(i->parent_folder_id) !=
602           folder_id_to_id_map.end());
603    history::StarID parent_id = folder_id_to_id_map[i->parent_folder_id];
604    BookmarkNode* parent = id_to_node_map[parent_id];
605    if (!parent) {
606      // Haven't encountered the parent yet, create it now.
607      parent = new BookmarkNode(0, GURL());
608      id_to_node_map[parent_id] = parent;
609    }
610
611    // Add the node to its parent. |entries| is ordered by parent then
612    // visual order so that we know we maintain visual order by always adding
613    // to the end.
614    parent->Add(node, parent->child_count());
615  }
616
617  // Save to file.
618  BookmarkCodec encoder;
619  scoped_ptr<Value> encoded_bookmarks(
620      encoder.Encode(&bookmark_bar_node, &other_node));
621  std::string content;
622  base::JSONWriter::Write(encoded_bookmarks.get(), true, &content);
623
624  return (file_util::WriteFile(path, content.c_str(),
625                               static_cast<int>(content.length())) != -1);
626}
627
628}  // namespace history
629