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 42 namespace history { 43 44 namespace { 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 " 51 const char kHistoryStarFields[] = STAR_FIELDS; 52 53 void 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 85 StarredURLDatabase::StarredURLDatabase() { 86 } 87 88 StarredURLDatabase::~StarredURLDatabase() { 89 } 90 91 bool 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 107 bool 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 133 bool 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 152 bool 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 172 bool 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 188 StarID 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 232 bool 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 242 bool 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 259 StarID 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 304 UIStarID 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 318 bool 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 393 StarredURLDatabase::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 404 bool 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 423 bool 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 522 bool 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 536 bool 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