// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/history/starred_url_database.h"
#include "app/sql/statement.h"
#include "base/file_util.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/memory/scoped_vector.h"
#include "base/stl_util-inl.h"
#include "base/string_util.h"
#include "base/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/bookmarks/bookmark_codec.h"
#include "chrome/browser/bookmarks/bookmark_model.h"
#include "chrome/browser/history/history.h"
#include "chrome/browser/history/query_parser.h"
// The following table is used to store star (aka bookmark) information. This
// class derives from URLDatabase, which has its own schema.
//
// starred
// id Unique identifier (primary key) for the entry.
// type Type of entry, if 0 this corresponds to a URL, 1 for
// a system folder, 2 for a user created folder, 3 for
// other.
// url_id ID of the url, only valid if type == 0
// group_id ID of the folder, only valid if type != 0. This id comes
// from the UI and is NOT the same as id.
// title User assigned title.
// date_added Creation date.
// visual_order Visual order within parent.
// parent_id Folder ID of the parent this entry is contained in, if 0
// entry is not in a folder.
// date_modified Time the folder was last modified. See comments in
// StarredEntry::date_folder_modified
// NOTE: group_id and parent_id come from the UI, id is assigned by the
// db.
namespace history {
namespace {
// Fields used by FillInStarredEntry.
#define STAR_FIELDS \
" starred.id, starred.type, starred.title, starred.date_added, " \
"starred.visual_order, starred.parent_id, urls.url, urls.id, " \
"starred.group_id, starred.date_modified "
const char kHistoryStarFields[] = STAR_FIELDS;
void FillInStarredEntry(const sql::Statement& s, StarredEntry* entry) {
DCHECK(entry);
entry->id = s.ColumnInt64(0);
switch (s.ColumnInt(1)) {
case 0:
entry->type = history::StarredEntry::URL;
entry->url = GURL(s.ColumnString(6));
break;
case 1:
entry->type = history::StarredEntry::BOOKMARK_BAR;
break;
case 2:
entry->type = history::StarredEntry::USER_FOLDER;
break;
case 3:
entry->type = history::StarredEntry::OTHER;
break;
default:
NOTREACHED();
break;
}
entry->title = s.ColumnString16(2);
entry->date_added = base::Time::FromInternalValue(s.ColumnInt64(3));
entry->visual_order = s.ColumnInt(4);
entry->parent_folder_id = s.ColumnInt64(5);
entry->url_id = s.ColumnInt64(7);
entry->folder_id = s.ColumnInt64(8);
entry->date_folder_modified = base::Time::FromInternalValue(s.ColumnInt64(9));
}
} // namespace
StarredURLDatabase::StarredURLDatabase() {
}
StarredURLDatabase::~StarredURLDatabase() {
}
bool StarredURLDatabase::MigrateBookmarksToFile(const FilePath& path) {
if (!GetDB().DoesTableExist("starred"))
return true;
if (EnsureStarredIntegrity() && !MigrateBookmarksToFileImpl(path)) {
NOTREACHED() << " Bookmarks migration failed";
return false;
}
if (!GetDB().Execute("DROP TABLE starred")) {
NOTREACHED() << "Unable to drop starred table";
return false;
}
return true;
}
bool StarredURLDatabase::GetAllStarredEntries(
std::vector<StarredEntry>* entries) {
DCHECK(entries);
std::string sql = "SELECT ";
sql.append(kHistoryStarFields);
sql.append("FROM starred LEFT JOIN urls ON starred.url_id = urls.id ");
sql += "ORDER BY parent_id, visual_order";
sql::Statement s(GetDB().GetUniqueStatement(sql.c_str()));
if (!s) {
NOTREACHED() << "Statement prepare failed";
return false;
}
history::StarredEntry entry;
while (s.Step()) {
FillInStarredEntry(s, &entry);
// Reset the url for non-url types. This is needed as we're reusing the
// same entry for the loop.
if (entry.type != history::StarredEntry::URL)
entry.url = GURL();
entries->push_back(entry);
}
return true;
}
bool StarredURLDatabase::EnsureStarredIntegrity() {
std::set<StarredNode*> roots;
std::set<StarID> folders_with_duplicate_ids;
std::set<StarredNode*> unparented_urls;
std::set<StarID> empty_url_ids;
if (!BuildStarNodes(&roots, &folders_with_duplicate_ids, &unparented_urls,
&empty_url_ids)) {
return false;
}
bool valid = EnsureStarredIntegrityImpl(&roots, folders_with_duplicate_ids,
&unparented_urls, empty_url_ids);
STLDeleteElements(&roots);
STLDeleteElements(&unparented_urls);
return valid;
}
bool StarredURLDatabase::UpdateStarredEntryRow(StarID star_id,
const string16& title,
UIStarID parent_folder_id,
int visual_order,
base::Time date_modified) {
DCHECK(star_id && visual_order >= 0);
sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
"UPDATE starred SET title=?, parent_id=?, visual_order=?, "
"date_modified=? WHERE id=?"));
if (!statement)
return 0;
statement.BindString16(0, title);
statement.BindInt64(1, parent_folder_id);
statement.BindInt(2, visual_order);
statement.BindInt64(3, date_modified.ToInternalValue());
statement.BindInt64(4, star_id);
return statement.Run();
}
bool StarredURLDatabase::AdjustStarredVisualOrder(UIStarID parent_folder_id,
int start_visual_order,
int delta) {
DCHECK(parent_folder_id && start_visual_order >= 0);
sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
"UPDATE starred SET visual_order=visual_order+? "
"WHERE parent_id=? AND visual_order >= ?"));
if (!statement)
return false;
statement.BindInt(0, delta);
statement.BindInt64(1, parent_folder_id);
statement.BindInt(2, start_visual_order);
return statement.Run();
}
StarID StarredURLDatabase::CreateStarredEntryRow(URLID url_id,
UIStarID folder_id,
UIStarID parent_folder_id,
const string16& title,
const base::Time& date_added,
int visual_order,
StarredEntry::Type type) {
DCHECK(visual_order >= 0 &&
(type != history::StarredEntry::URL || url_id));
sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
"INSERT INTO starred "
"(type, url_id, group_id, title, date_added, visual_order, parent_id, "
"date_modified) VALUES (?,?,?,?,?,?,?,?)"));
if (!statement)
return 0;
switch (type) {
case history::StarredEntry::URL:
statement.BindInt(0, 0);
break;
case history::StarredEntry::BOOKMARK_BAR:
statement.BindInt(0, 1);
break;
case history::StarredEntry::USER_FOLDER:
statement.BindInt(0, 2);
break;
case history::StarredEntry::OTHER:
statement.BindInt(0, 3);
break;
default:
NOTREACHED();
}
statement.BindInt64(1, url_id);
statement.BindInt64(2, folder_id);
statement.BindString16(3, title);
statement.BindInt64(4, date_added.ToInternalValue());
statement.BindInt(5, visual_order);
statement.BindInt64(6, parent_folder_id);
statement.BindInt64(7, base::Time().ToInternalValue());
if (statement.Run())
return GetDB().GetLastInsertRowId();
return 0;
}
bool StarredURLDatabase::DeleteStarredEntryRow(StarID star_id) {
sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
"DELETE FROM starred WHERE id=?"));
if (!statement)
return false;
statement.BindInt64(0, star_id);
return statement.Run();
}
bool StarredURLDatabase::GetStarredEntry(StarID star_id, StarredEntry* entry) {
DCHECK(entry && star_id);
sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
"SELECT" STAR_FIELDS "FROM starred LEFT JOIN urls ON "
"starred.url_id = urls.id WHERE starred.id=?"));
if (!statement)
return false;
statement.BindInt64(0, star_id);
if (statement.Step()) {
FillInStarredEntry(statement, entry);
return true;
}
return false;
}
StarID StarredURLDatabase::CreateStarredEntry(StarredEntry* entry) {
entry->id = 0; // Ensure 0 for failure case.
// Adjust the visual order when we are inserting it somewhere.
if (entry->parent_folder_id)
AdjustStarredVisualOrder(entry->parent_folder_id, entry->visual_order, 1);
// Insert the new entry.
switch (entry->type) {
case StarredEntry::USER_FOLDER:
entry->id = CreateStarredEntryRow(0, entry->folder_id,
entry->parent_folder_id, entry->title, entry->date_added,
entry->visual_order, entry->type);
break;
case StarredEntry::URL: {
// Get the row for this URL.
URLRow url_row;
if (!GetRowForURL(entry->url, &url_row)) {
// Create a new URL row for this entry.
url_row = URLRow(entry->url);
url_row.set_title(entry->title);
url_row.set_hidden(false);
entry->url_id = this->AddURL(url_row);
} else {
entry->url_id = url_row.id(); // The caller doesn't have to set this.
}
// Create the star entry referring to the URL row.
entry->id = CreateStarredEntryRow(entry->url_id, entry->folder_id,
entry->parent_folder_id, entry->title, entry->date_added,
entry->visual_order, entry->type);
// Update the URL row to refer to this new starred entry.
UpdateURLRow(entry->url_id, url_row);
break;
}
default:
NOTREACHED();
break;
}
return entry->id;
}
UIStarID StarredURLDatabase::GetMaxFolderID() {
sql::Statement max_folder_id_statement(GetDB().GetUniqueStatement(
"SELECT MAX(group_id) FROM starred"));
if (!max_folder_id_statement) {
NOTREACHED() << GetDB().GetErrorMessage();
return 0;
}
if (!max_folder_id_statement.Step()) {
NOTREACHED() << GetDB().GetErrorMessage();
return 0;
}
return max_folder_id_statement.ColumnInt64(0);
}
bool StarredURLDatabase::BuildStarNodes(
std::set<StarredURLDatabase::StarredNode*>* roots,
std::set<StarID>* folders_with_duplicate_ids,
std::set<StarredNode*>* unparented_urls,
std::set<StarID>* empty_url_ids) {
std::vector<StarredEntry> star_entries;
if (!GetAllStarredEntries(&star_entries)) {
NOTREACHED() << "Unable to get bookmarks from database";
return false;
}
// Create the folder/bookmark-bar/other nodes.
std::map<UIStarID, StarredNode*> folder_id_to_node_map;
for (size_t i = 0; i < star_entries.size(); ++i) {
if (star_entries[i].type != StarredEntry::URL) {
if (folder_id_to_node_map.find(star_entries[i].folder_id) !=
folder_id_to_node_map.end()) {
// There's already a folder with this ID.
folders_with_duplicate_ids->insert(star_entries[i].id);
} else {
// Create the node and update the mapping.
StarredNode* node = new StarredNode(star_entries[i]);
folder_id_to_node_map[star_entries[i].folder_id] = node;
}
}
}
// Iterate again, creating nodes for URL bookmarks and parenting all
// bookmarks/folders. In addition populate the empty_url_ids with all entries
// of type URL that have an empty URL.
std::map<StarID, StarredNode*> id_to_node_map;
for (size_t i = 0; i < star_entries.size(); ++i) {
if (star_entries[i].type == StarredEntry::URL) {
if (star_entries[i].url.is_empty()) {
empty_url_ids->insert(star_entries[i].id);
} else if (!star_entries[i].parent_folder_id ||
folder_id_to_node_map.find(star_entries[i].parent_folder_id) ==
folder_id_to_node_map.end()) {
// This entry has no parent, or we couldn't find the parent.
StarredNode* node = new StarredNode(star_entries[i]);
unparented_urls->insert(node);
} else {
// Add the node to its parent.
StarredNode* parent =
folder_id_to_node_map[star_entries[i].parent_folder_id];
StarredNode* node = new StarredNode(star_entries[i]);
parent->Add(node, parent->child_count());
}
} else if (folders_with_duplicate_ids->find(star_entries[i].id) ==
folders_with_duplicate_ids->end()) {
// The entry is a folder (or bookmark bar/other node) that isn't
// marked as a duplicate.
if (!star_entries[i].parent_folder_id ||
folder_id_to_node_map.find(star_entries[i].parent_folder_id) ==
folder_id_to_node_map.end()) {
// Entry has no parent, or the parent wasn't found.
roots->insert(folder_id_to_node_map[star_entries[i].folder_id]);
} else {
// Parent the folder node.
StarredNode* parent =
folder_id_to_node_map[star_entries[i].parent_folder_id];
StarredNode* node = folder_id_to_node_map[star_entries[i].folder_id];
if (!node->HasAncestor(parent) && !parent->HasAncestor(node)) {
parent->Add(node, parent->child_count());
} else {
// The node has a cycle. Add it to the list of roots so the cycle is
// broken.
roots->insert(node);
}
}
}
}
return true;
}
StarredURLDatabase::StarredNode* StarredURLDatabase::GetNodeByType(
const std::set<StarredURLDatabase::StarredNode*>& nodes,
StarredEntry::Type type) {
for (std::set<StarredNode*>::const_iterator i = nodes.begin();
i != nodes.end(); ++i) {
if ((*i)->value.type == type)
return *i;
}
return NULL;
}
bool StarredURLDatabase::EnsureVisualOrder(
StarredURLDatabase::StarredNode* node) {
for (int i = 0; i < node->child_count(); ++i) {
if (node->GetChild(i)->value.visual_order != i) {
StarredEntry& entry = node->GetChild(i)->value;
entry.visual_order = i;
LOG(WARNING) << "Bookmark visual order is wrong";
if (!UpdateStarredEntryRow(entry.id, entry.title, entry.parent_folder_id,
i, entry.date_folder_modified)) {
NOTREACHED() << "Unable to update visual order";
return false;
}
}
if (!EnsureVisualOrder(node->GetChild(i)))
return false;
}
return true;
}
bool StarredURLDatabase::EnsureStarredIntegrityImpl(
std::set<StarredURLDatabase::StarredNode*>* roots,
const std::set<StarID>& folders_with_duplicate_ids,
std::set<StarredNode*>* unparented_urls,
const std::set<StarID>& empty_url_ids) {
// Make sure the bookmark bar entry exists.
StarredNode* bookmark_node =
GetNodeByType(*roots, StarredEntry::BOOKMARK_BAR);
if (!bookmark_node) {
LOG(WARNING) << "No bookmark bar folder in database";
// If there is no bookmark bar entry in the db things are really
// screwed. Return false, which won't trigger migration and we'll just
// drop the tables.
return false;
}
// Make sure the other node exists.
StarredNode* other_node = GetNodeByType(*roots, StarredEntry::OTHER);
if (!other_node) {
LOG(WARNING) << "No bookmark other folder in database";
StarredEntry entry;
entry.folder_id = GetMaxFolderID() + 1;
if (entry.folder_id == 1) {
NOTREACHED() << "Unable to get new id for other bookmarks folder";
return false;
}
entry.id = CreateStarredEntryRow(
0, entry.folder_id, 0, UTF8ToUTF16("other"), base::Time::Now(), 0,
history::StarredEntry::OTHER);
if (!entry.id) {
NOTREACHED() << "Unable to create other bookmarks folder";
return false;
}
entry.type = StarredEntry::OTHER;
StarredNode* other_node = new StarredNode(entry);
roots->insert(other_node);
}
// We could potentially make sure only one folder with type
// BOOKMARK_BAR/OTHER, but history backend enforces this.
// Nuke any entries with no url.
for (std::set<StarID>::const_iterator i = empty_url_ids.begin();
i != empty_url_ids.end(); ++i) {
LOG(WARNING) << "Bookmark exists with no URL";
if (!DeleteStarredEntryRow(*i)) {
NOTREACHED() << "Unable to delete bookmark";
return false;
}
}
// Make sure the visual order of the nodes is correct.
for (std::set<StarredNode*>::const_iterator i = roots->begin();
i != roots->end(); ++i) {
if (!EnsureVisualOrder(*i))
return false;
}
// Move any unparented bookmarks to the bookmark bar.
{
std::set<StarredNode*>::iterator i = unparented_urls->begin();
while (i != unparented_urls->end()) {
LOG(WARNING) << "Bookmark not in a bookmark folder found";
if (!Move(*i, bookmark_node))
return false;
unparented_urls->erase(i++);
}
}
// Nuke any folders with duplicate ids. A duplicate id means there are two
// folders in the starred table with the same folder_id. We only keep the
// first folder, all other folders are removed.
for (std::set<StarID>::const_iterator i = folders_with_duplicate_ids.begin();
i != folders_with_duplicate_ids.end(); ++i) {
LOG(WARNING) << "Duplicate folder id in bookmark database";
if (!DeleteStarredEntryRow(*i)) {
NOTREACHED() << "Unable to delete folder";
return false;
}
}
// Move unparented user folders back to the bookmark bar.
{
std::set<StarredNode*>::iterator i = roots->begin();
while (i != roots->end()) {
if ((*i)->value.type == StarredEntry::USER_FOLDER) {
LOG(WARNING) << "Bookmark folder not on bookmark bar found";
if (!Move(*i, bookmark_node))
return false;
roots->erase(i++);
} else {
++i;
}
}
}
return true;
}
bool StarredURLDatabase::Move(StarredNode* source, StarredNode* new_parent) {
history::StarredEntry& entry = source->value;
entry.visual_order = new_parent->child_count();
entry.parent_folder_id = new_parent->value.folder_id;
if (!UpdateStarredEntryRow(entry.id, entry.title,
entry.parent_folder_id, entry.visual_order,
entry.date_folder_modified)) {
NOTREACHED() << "Unable to move folder";
return false;
}
new_parent->Add(source, new_parent->child_count());
return true;
}
bool StarredURLDatabase::MigrateBookmarksToFileImpl(const FilePath& path) {
std::vector<history::StarredEntry> entries;
if (!GetAllStarredEntries(&entries))
return false;
// Create the bookmark bar and other folder nodes.
history::StarredEntry entry;
entry.type = history::StarredEntry::BOOKMARK_BAR;
BookmarkNode bookmark_bar_node(0, GURL());
bookmark_bar_node.Reset(entry);
entry.type = history::StarredEntry::OTHER;
BookmarkNode other_node(0, GURL());
other_node.Reset(entry);
std::map<history::UIStarID, history::StarID> folder_id_to_id_map;
typedef std::map<history::StarID, BookmarkNode*> IDToNodeMap;
IDToNodeMap id_to_node_map;
history::UIStarID other_folder_folder_id = 0;
history::StarID other_folder_id = 0;
// Iterate through the entries building a mapping between folder_id and id.
for (std::vector<history::StarredEntry>::const_iterator i = entries.begin();
i != entries.end(); ++i) {
if (i->type != history::StarredEntry::URL) {
folder_id_to_id_map[i->folder_id] = i->id;
if (i->type == history::StarredEntry::OTHER) {
other_folder_id = i->id;
other_folder_folder_id = i->folder_id;
}
}
}
// Register the bookmark bar and other folder nodes in the maps.
id_to_node_map[HistoryService::kBookmarkBarID] = &bookmark_bar_node;
folder_id_to_id_map[HistoryService::kBookmarkBarID] =
HistoryService::kBookmarkBarID;
if (other_folder_folder_id) {
id_to_node_map[other_folder_id] = &other_node;
folder_id_to_id_map[other_folder_folder_id] = other_folder_id;
}
// Iterate through the entries again creating the nodes.
for (std::vector<history::StarredEntry>::iterator i = entries.begin();
i != entries.end(); ++i) {
if (!i->parent_folder_id) {
DCHECK(i->type == history::StarredEntry::BOOKMARK_BAR ||
i->type == history::StarredEntry::OTHER);
// Only entries with no parent should be the bookmark bar and other
// bookmarks folders.
continue;
}
BookmarkNode* node = id_to_node_map[i->id];
if (!node) {
// Creating a node results in creating the parent. As such, it is
// possible for the node representing a folder to have been created before
// encountering the details.
// The created nodes are owned by the root node.
node = new BookmarkNode(0, i->url);
id_to_node_map[i->id] = node;
}
node->Reset(*i);
DCHECK(folder_id_to_id_map.find(i->parent_folder_id) !=
folder_id_to_id_map.end());
history::StarID parent_id = folder_id_to_id_map[i->parent_folder_id];
BookmarkNode* parent = id_to_node_map[parent_id];
if (!parent) {
// Haven't encountered the parent yet, create it now.
parent = new BookmarkNode(0, GURL());
id_to_node_map[parent_id] = parent;
}
// Add the node to its parent. |entries| is ordered by parent then
// visual order so that we know we maintain visual order by always adding
// to the end.
parent->Add(node, parent->child_count());
}
// Save to file.
BookmarkCodec encoder;
scoped_ptr<Value> encoded_bookmarks(
encoder.Encode(&bookmark_bar_node, &other_node));
std::string content;
base::JSONWriter::Write(encoded_bookmarks.get(), true, &content);
return (file_util::WriteFile(path, content.c_str(),
static_cast<int>(content.length())) != -1);
}
} // namespace history