1 // Copyright (c) 2012 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/sync/glue/typed_url_model_associator.h" 6 7 #include <algorithm> 8 #include <set> 9 10 #include "base/location.h" 11 #include "base/logging.h" 12 #include "base/metrics/histogram.h" 13 #include "base/strings/utf_string_conversions.h" 14 #include "chrome/browser/history/history_backend.h" 15 #include "chrome/browser/sync/profile_sync_service.h" 16 #include "content/public/browser/browser_thread.h" 17 #include "net/base/net_util.h" 18 #include "sync/api/sync_error.h" 19 #include "sync/internal_api/public/read_node.h" 20 #include "sync/internal_api/public/read_transaction.h" 21 #include "sync/internal_api/public/write_node.h" 22 #include "sync/internal_api/public/write_transaction.h" 23 #include "sync/protocol/typed_url_specifics.pb.h" 24 25 using content::BrowserThread; 26 27 namespace browser_sync { 28 29 // The server backend can't handle arbitrarily large node sizes, so to keep 30 // the size under control we limit the visit array. 31 static const int kMaxTypedUrlVisits = 100; 32 33 // There's no limit on how many visits the history DB could have for a given 34 // typed URL, so we limit how many we fetch from the DB to avoid crashes due to 35 // running out of memory (http://crbug.com/89793). This value is different 36 // from kMaxTypedUrlVisits, as some of the visits fetched from the DB may be 37 // RELOAD visits, which will be stripped. 38 static const int kMaxVisitsToFetch = 1000; 39 40 static bool CheckVisitOrdering(const history::VisitVector& visits) { 41 int64 previous_visit_time = 0; 42 for (history::VisitVector::const_iterator visit = visits.begin(); 43 visit != visits.end(); ++visit) { 44 if (visit != visits.begin()) { 45 // We allow duplicate visits here - they shouldn't really be allowed, but 46 // they still seem to show up sometimes and we haven't figured out the 47 // source, so we just log an error instead of failing an assertion. 48 // (http://crbug.com/91473). 49 if (previous_visit_time == visit->visit_time.ToInternalValue()) 50 DVLOG(1) << "Duplicate visit time encountered"; 51 else if (previous_visit_time > visit->visit_time.ToInternalValue()) 52 return false; 53 } 54 55 previous_visit_time = visit->visit_time.ToInternalValue(); 56 } 57 return true; 58 } 59 60 TypedUrlModelAssociator::TypedUrlModelAssociator( 61 ProfileSyncService* sync_service, 62 history::HistoryBackend* history_backend, 63 DataTypeErrorHandler* error_handler) 64 : sync_service_(sync_service), 65 history_backend_(history_backend), 66 expected_loop_(base::MessageLoop::current()), 67 abort_requested_(false), 68 error_handler_(error_handler), 69 num_db_accesses_(0), 70 num_db_errors_(0) { 71 DCHECK(sync_service_); 72 // history_backend_ may be null for unit tests (since it's not mockable). 73 DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::UI)); 74 } 75 76 TypedUrlModelAssociator::~TypedUrlModelAssociator() {} 77 78 79 bool TypedUrlModelAssociator::FixupURLAndGetVisits( 80 history::URLRow* url, 81 history::VisitVector* visits) { 82 ++num_db_accesses_; 83 CHECK(history_backend_); 84 if (!history_backend_->GetMostRecentVisitsForURL( 85 url->id(), kMaxVisitsToFetch, visits)) { 86 ++num_db_errors_; 87 return false; 88 } 89 90 // Sometimes (due to a bug elsewhere in the history or sync code, or due to 91 // a crash between adding a URL to the history database and updating the 92 // visit DB) the visit vector for a URL can be empty. If this happens, just 93 // create a new visit whose timestamp is the same as the last_visit time. 94 // This is a workaround for http://crbug.com/84258. 95 if (visits->empty()) { 96 DVLOG(1) << "Found empty visits for URL: " << url->url(); 97 98 if (url->last_visit().is_null()) { 99 // If modified URL is bookmarked, history backend treats it as modified 100 // even if all its visits are deleted. Return false to stop further 101 // processing because sync expects valid visit time for modified entry. 102 return false; 103 } 104 105 history::VisitRow visit( 106 url->id(), url->last_visit(), 0, content::PAGE_TRANSITION_TYPED, 0); 107 visits->push_back(visit); 108 } 109 110 // GetMostRecentVisitsForURL() returns the data in the opposite order that 111 // we need it, so reverse it. 112 std::reverse(visits->begin(), visits->end()); 113 114 // Sometimes, the last_visit field in the URL doesn't match the timestamp of 115 // the last visit in our visit array (they come from different tables, so 116 // crashes/bugs can cause them to mismatch), so just set it here. 117 url->set_last_visit(visits->back().visit_time); 118 DCHECK(CheckVisitOrdering(*visits)); 119 return true; 120 } 121 122 bool TypedUrlModelAssociator::ShouldIgnoreUrl(const GURL& url) { 123 // Ignore empty URLs. Not sure how this can happen (maybe import from other 124 // busted browsers, or misuse of the history API, or just plain bugs) but we 125 // can't deal with them. 126 if (url.spec().empty()) 127 return true; 128 129 // Ignore local file URLs. 130 if (url.SchemeIsFile()) 131 return true; 132 133 // Ignore localhost URLs. 134 if (net::IsLocalhost(url.host())) 135 return true; 136 137 return false; 138 } 139 140 bool TypedUrlModelAssociator::ShouldIgnoreVisits( 141 const history::VisitVector& visits) { 142 // We ignore URLs that were imported, but have never been visited by 143 // chromium. 144 static const int kLastImportedSource = history::SOURCE_EXTENSION; 145 history::VisitSourceMap map; 146 if (!history_backend_->GetVisitsSource(visits, &map)) 147 return false; // If we can't read the visit, assume it's not imported. 148 149 // Walk the list of visits and look for a non-imported item. 150 for (history::VisitVector::const_iterator it = visits.begin(); 151 it != visits.end(); ++it) { 152 if (map.count(it->visit_id) == 0 || 153 map[it->visit_id] <= kLastImportedSource) { 154 return false; 155 } 156 } 157 // We only saw imported visits, so tell the caller to ignore them. 158 return true; 159 } 160 161 syncer::SyncError TypedUrlModelAssociator::AssociateModels( 162 syncer::SyncMergeResult* local_merge_result, 163 syncer::SyncMergeResult* syncer_merge_result) { 164 ClearErrorStats(); 165 syncer::SyncError error = DoAssociateModels(); 166 UMA_HISTOGRAM_PERCENTAGE("Sync.TypedUrlModelAssociationErrors", 167 GetErrorPercentage()); 168 ClearErrorStats(); 169 return error; 170 } 171 172 void TypedUrlModelAssociator::ClearErrorStats() { 173 num_db_accesses_ = 0; 174 num_db_errors_ = 0; 175 } 176 177 int TypedUrlModelAssociator::GetErrorPercentage() const { 178 return num_db_accesses_ ? (100 * num_db_errors_ / num_db_accesses_) : 0; 179 } 180 181 syncer::SyncError TypedUrlModelAssociator::DoAssociateModels() { 182 DVLOG(1) << "Associating TypedUrl Models"; 183 DCHECK(expected_loop_ == base::MessageLoop::current()); 184 185 history::URLRows typed_urls; 186 ++num_db_accesses_; 187 bool query_succeeded = 188 history_backend_ && history_backend_->GetAllTypedURLs(&typed_urls); 189 190 history::URLRows new_urls; 191 TypedUrlVisitVector new_visits; 192 TypedUrlUpdateVector updated_urls; 193 { 194 base::AutoLock au(abort_lock_); 195 if (abort_requested_) { 196 return syncer::SyncError(FROM_HERE, 197 syncer::SyncError::DATATYPE_ERROR, 198 "Association was aborted.", 199 model_type()); 200 } 201 202 // Must lock and check first to make sure |error_handler_| is valid. 203 if (!query_succeeded) { 204 ++num_db_errors_; 205 return error_handler_->CreateAndUploadError( 206 FROM_HERE, 207 "Could not get the typed_url entries.", 208 model_type()); 209 } 210 211 // Get all the visits. 212 std::map<history::URLID, history::VisitVector> visit_vectors; 213 for (history::URLRows::iterator ix = typed_urls.begin(); 214 ix != typed_urls.end();) { 215 DCHECK_EQ(0U, visit_vectors.count(ix->id())); 216 if (!FixupURLAndGetVisits(&(*ix), &(visit_vectors[ix->id()])) || 217 ShouldIgnoreUrl(ix->url()) || 218 ShouldIgnoreVisits(visit_vectors[ix->id()])) { 219 // Ignore this URL if we couldn't load the visits or if there's some 220 // other problem with it (it was empty, or imported and never visited). 221 ix = typed_urls.erase(ix); 222 } else { 223 ++ix; 224 } 225 } 226 227 syncer::WriteTransaction trans(FROM_HERE, sync_service_->GetUserShare()); 228 syncer::ReadNode typed_url_root(&trans); 229 if (typed_url_root.InitTypeRoot(syncer::TYPED_URLS) != 230 syncer::BaseNode::INIT_OK) { 231 return error_handler_->CreateAndUploadError( 232 FROM_HERE, 233 "Server did not create the top-level typed_url node. We " 234 "might be running against an out-of-date server.", 235 model_type()); 236 } 237 238 std::set<std::string> current_urls; 239 for (history::URLRows::iterator ix = typed_urls.begin(); 240 ix != typed_urls.end(); ++ix) { 241 std::string tag = ix->url().spec(); 242 // Empty URLs should be filtered out by ShouldIgnoreUrl() previously. 243 DCHECK(!tag.empty()); 244 history::VisitVector& visits = visit_vectors[ix->id()]; 245 246 syncer::ReadNode node(&trans); 247 if (node.InitByClientTagLookup(syncer::TYPED_URLS, tag) == 248 syncer::BaseNode::INIT_OK) { 249 // Same URL exists in sync data and in history data - compare the 250 // entries to see if there's any difference. 251 sync_pb::TypedUrlSpecifics typed_url( 252 FilterExpiredVisits(node.GetTypedUrlSpecifics())); 253 DCHECK_EQ(tag, typed_url.url()); 254 255 // Initialize fields in |new_url| to the same values as the fields in 256 // the existing URLRow in the history DB. This is needed because we 257 // overwrite the existing value below in WriteToHistoryBackend(), but 258 // some of the values in that structure are not synced (like 259 // typed_count). 260 history::URLRow new_url(*ix); 261 262 std::vector<history::VisitInfo> added_visits; 263 MergeResult difference = 264 MergeUrls(typed_url, *ix, &visits, &new_url, &added_visits); 265 if (difference & DIFF_UPDATE_NODE) { 266 syncer::WriteNode write_node(&trans); 267 if (write_node.InitByClientTagLookup(syncer::TYPED_URLS, tag) != 268 syncer::BaseNode::INIT_OK) { 269 return error_handler_->CreateAndUploadError( 270 FROM_HERE, 271 "Failed to edit typed_url sync node.", 272 model_type()); 273 } 274 // We don't want to resurrect old visits that have been aged out by 275 // other clients, so remove all visits that are older than the 276 // earliest existing visit in the sync node. 277 if (typed_url.visits_size() > 0) { 278 base::Time earliest_visit = 279 base::Time::FromInternalValue(typed_url.visits(0)); 280 for (history::VisitVector::iterator it = visits.begin(); 281 it != visits.end() && it->visit_time < earliest_visit; ) { 282 it = visits.erase(it); 283 } 284 // Should never be possible to delete all the items, since the 285 // visit vector contains all the items in typed_url.visits. 286 DCHECK(visits.size() > 0); 287 } 288 DCHECK_EQ(new_url.last_visit().ToInternalValue(), 289 visits.back().visit_time.ToInternalValue()); 290 WriteToSyncNode(new_url, visits, &write_node); 291 } 292 if (difference & DIFF_LOCAL_ROW_CHANGED) { 293 updated_urls.push_back( 294 std::pair<history::URLID, history::URLRow>(ix->id(), new_url)); 295 } 296 if (difference & DIFF_LOCAL_VISITS_ADDED) { 297 new_visits.push_back( 298 std::pair<GURL, std::vector<history::VisitInfo> >(ix->url(), 299 added_visits)); 300 } 301 } else { 302 // Sync has never seen this URL before. 303 syncer::WriteNode node(&trans); 304 syncer::WriteNode::InitUniqueByCreationResult result = 305 node.InitUniqueByCreation(syncer::TYPED_URLS, 306 typed_url_root, tag); 307 if (result != syncer::WriteNode::INIT_SUCCESS) { 308 return error_handler_->CreateAndUploadError( 309 FROM_HERE, 310 "Failed to create typed_url sync node: " + tag, 311 model_type()); 312 } 313 314 node.SetTitle(tag); 315 WriteToSyncNode(*ix, visits, &node); 316 } 317 318 current_urls.insert(tag); 319 } 320 321 // Now walk the sync nodes and detect any URLs that exist there, but not in 322 // the history DB, so we can add them to our local history DB. 323 std::vector<int64> obsolete_nodes; 324 int64 sync_child_id = typed_url_root.GetFirstChildId(); 325 while (sync_child_id != syncer::kInvalidId) { 326 syncer::ReadNode sync_child_node(&trans); 327 if (sync_child_node.InitByIdLookup(sync_child_id) != 328 syncer::BaseNode::INIT_OK) { 329 return error_handler_->CreateAndUploadError( 330 FROM_HERE, 331 "Failed to fetch child node.", 332 model_type()); 333 } 334 const sync_pb::TypedUrlSpecifics& typed_url( 335 sync_child_node.GetTypedUrlSpecifics()); 336 337 sync_child_id = sync_child_node.GetSuccessorId(); 338 339 // Ignore old sync nodes that don't have any transition data stored with 340 // them, or transition data that does not match the visit data (will be 341 // deleted below). 342 if (typed_url.visit_transitions_size() == 0 || 343 typed_url.visit_transitions_size() != typed_url.visits_size()) { 344 // Generate a debug assertion to help track down http://crbug.com/91473, 345 // even though we gracefully handle this case by throwing away this 346 // node. 347 DCHECK_EQ(typed_url.visits_size(), typed_url.visit_transitions_size()); 348 DVLOG(1) << "Deleting obsolete sync node with no visit " 349 << "transition info."; 350 obsolete_nodes.push_back(sync_child_node.GetId()); 351 continue; 352 } 353 354 if (typed_url.url().empty()) { 355 DVLOG(1) << "Ignoring empty URL in sync DB"; 356 continue; 357 } 358 359 // Now, get rid of the expired visits, and if there are no un-expired 360 // visits left, just ignore this node. 361 sync_pb::TypedUrlSpecifics filtered_url = FilterExpiredVisits(typed_url); 362 if (filtered_url.visits_size() == 0) { 363 DVLOG(1) << "Ignoring expired URL in sync DB: " << filtered_url.url(); 364 continue; 365 } 366 367 if (current_urls.find(filtered_url.url()) == current_urls.end()) { 368 // Update the local DB from the sync DB. Since we are doing our 369 // initial model association, we don't want to remove any of the 370 // existing visits (pass NULL as |visits_to_remove|). 371 UpdateFromSyncDB(filtered_url, 372 &new_visits, 373 NULL, 374 &updated_urls, 375 &new_urls); 376 } 377 } 378 379 // If we encountered any obsolete nodes, remove them so they don't hang 380 // around and confuse people looking at the sync node browser. 381 if (!obsolete_nodes.empty()) { 382 for (std::vector<int64>::const_iterator it = obsolete_nodes.begin(); 383 it != obsolete_nodes.end(); 384 ++it) { 385 syncer::WriteNode sync_node(&trans); 386 if (sync_node.InitByIdLookup(*it) != syncer::BaseNode::INIT_OK) { 387 return error_handler_->CreateAndUploadError( 388 FROM_HERE, 389 "Failed to fetch obsolete node.", 390 model_type()); 391 } 392 sync_node.Tombstone(); 393 } 394 } 395 } 396 397 // Since we're on the history thread, we don't have to worry about updating 398 // the history database after closing the write transaction, since 399 // this is the only thread that writes to the database. We also don't have 400 // to worry about the sync model getting out of sync, because changes are 401 // propagated to the ChangeProcessor on this thread. 402 WriteToHistoryBackend(&new_urls, &updated_urls, &new_visits, NULL); 403 return syncer::SyncError(); 404 } 405 406 void TypedUrlModelAssociator::UpdateFromSyncDB( 407 const sync_pb::TypedUrlSpecifics& typed_url, 408 TypedUrlVisitVector* visits_to_add, 409 history::VisitVector* visits_to_remove, 410 TypedUrlUpdateVector* updated_urls, 411 history::URLRows* new_urls) { 412 history::URLRow new_url(GURL(typed_url.url())); 413 history::VisitVector existing_visits; 414 bool existing_url = history_backend_->GetURL(new_url.url(), &new_url); 415 if (existing_url) { 416 // This URL already exists locally - fetch the visits so we can 417 // merge them below. 418 if (!FixupURLAndGetVisits(&new_url, &existing_visits)) { 419 // Couldn't load the visits for this URL due to some kind of DB error. 420 // Don't bother writing this URL to the history DB (if we ignore the 421 // error and continue, we might end up duplicating existing visits). 422 DLOG(ERROR) << "Could not load visits for url: " << new_url.url(); 423 return; 424 } 425 } 426 visits_to_add->push_back(std::pair<GURL, std::vector<history::VisitInfo> >( 427 new_url.url(), std::vector<history::VisitInfo>())); 428 429 // Update the URL with information from the typed URL. 430 UpdateURLRowFromTypedUrlSpecifics(typed_url, &new_url); 431 432 // Figure out which visits we need to add. 433 DiffVisits(existing_visits, typed_url, &visits_to_add->back().second, 434 visits_to_remove); 435 436 if (existing_url) { 437 updated_urls->push_back( 438 std::pair<history::URLID, history::URLRow>(new_url.id(), new_url)); 439 } else { 440 new_urls->push_back(new_url); 441 } 442 } 443 444 sync_pb::TypedUrlSpecifics TypedUrlModelAssociator::FilterExpiredVisits( 445 const sync_pb::TypedUrlSpecifics& source) { 446 // Make a copy of the source, then regenerate the visits. 447 sync_pb::TypedUrlSpecifics specifics(source); 448 specifics.clear_visits(); 449 specifics.clear_visit_transitions(); 450 for (int i = 0; i < source.visits_size(); ++i) { 451 base::Time time = base::Time::FromInternalValue(source.visits(i)); 452 if (!history_backend_->IsExpiredVisitTime(time)) { 453 specifics.add_visits(source.visits(i)); 454 specifics.add_visit_transitions(source.visit_transitions(i)); 455 } 456 } 457 DCHECK(specifics.visits_size() == specifics.visit_transitions_size()); 458 return specifics; 459 } 460 461 bool TypedUrlModelAssociator::DeleteAllNodes( 462 syncer::WriteTransaction* trans) { 463 DCHECK(expected_loop_ == base::MessageLoop::current()); 464 465 // Just walk through all our child nodes and delete them. 466 syncer::ReadNode typed_url_root(trans); 467 if (typed_url_root.InitTypeRoot(syncer::TYPED_URLS) != 468 syncer::BaseNode::INIT_OK) { 469 LOG(ERROR) << "Could not lookup root node"; 470 return false; 471 } 472 int64 sync_child_id = typed_url_root.GetFirstChildId(); 473 while (sync_child_id != syncer::kInvalidId) { 474 syncer::WriteNode sync_child_node(trans); 475 if (sync_child_node.InitByIdLookup(sync_child_id) != 476 syncer::BaseNode::INIT_OK) { 477 LOG(ERROR) << "Typed url node lookup failed."; 478 return false; 479 } 480 sync_child_id = sync_child_node.GetSuccessorId(); 481 sync_child_node.Tombstone(); 482 } 483 return true; 484 } 485 486 syncer::SyncError TypedUrlModelAssociator::DisassociateModels() { 487 return syncer::SyncError(); 488 } 489 490 void TypedUrlModelAssociator::AbortAssociation() { 491 base::AutoLock lock(abort_lock_); 492 abort_requested_ = true; 493 } 494 495 bool TypedUrlModelAssociator::SyncModelHasUserCreatedNodes(bool* has_nodes) { 496 DCHECK(has_nodes); 497 *has_nodes = false; 498 syncer::ReadTransaction trans(FROM_HERE, sync_service_->GetUserShare()); 499 syncer::ReadNode sync_node(&trans); 500 if (sync_node.InitTypeRoot(syncer::TYPED_URLS) != syncer::BaseNode::INIT_OK) { 501 LOG(ERROR) << "Server did not create the top-level typed_url node. We " 502 << "might be running against an out-of-date server."; 503 return false; 504 } 505 506 // The sync model has user created nodes if the typed_url folder has any 507 // children. 508 *has_nodes = sync_node.HasChildren(); 509 return true; 510 } 511 512 void TypedUrlModelAssociator::WriteToHistoryBackend( 513 const history::URLRows* new_urls, 514 const TypedUrlUpdateVector* updated_urls, 515 const TypedUrlVisitVector* new_visits, 516 const history::VisitVector* deleted_visits) { 517 if (new_urls) { 518 history_backend_->AddPagesWithDetails(*new_urls, history::SOURCE_SYNCED); 519 } 520 if (updated_urls) { 521 for (TypedUrlUpdateVector::const_iterator url = updated_urls->begin(); 522 url != updated_urls->end(); ++url) { 523 // This is an existing entry in the URL database. We don't verify the 524 // visit_count or typed_count values here, because either one (or both) 525 // could be zero in the case of bookmarks, or in the case of a URL 526 // transitioning from non-typed to typed as a result of this sync. 527 ++num_db_accesses_; 528 if (!history_backend_->UpdateURL(url->first, url->second)) { 529 // In the field we sometimes run into errors on specific URLs. It's OK 530 // to just continue on (we can try writing again on the next model 531 // association). 532 ++num_db_errors_; 533 DLOG(ERROR) << "Could not update page: " << url->second.url().spec(); 534 } 535 } 536 } 537 if (new_visits) { 538 for (TypedUrlVisitVector::const_iterator visits = new_visits->begin(); 539 visits != new_visits->end(); ++visits) { 540 // If there are no visits to add, just skip this. 541 if (visits->second.empty()) 542 continue; 543 ++num_db_accesses_; 544 if (!history_backend_->AddVisits(visits->first, visits->second, 545 history::SOURCE_SYNCED)) { 546 ++num_db_errors_; 547 DLOG(ERROR) << "Could not add visits."; 548 } 549 } 550 } 551 if (deleted_visits) { 552 ++num_db_accesses_; 553 if (!history_backend_->RemoveVisits(*deleted_visits)) { 554 ++num_db_errors_; 555 DLOG(ERROR) << "Could not remove visits."; 556 // This is bad news, since it means we may end up resurrecting history 557 // entries on the next reload. It's unavoidable so we'll just keep on 558 // syncing. 559 } 560 } 561 } 562 563 // static 564 TypedUrlModelAssociator::MergeResult TypedUrlModelAssociator::MergeUrls( 565 const sync_pb::TypedUrlSpecifics& node, 566 const history::URLRow& url, 567 history::VisitVector* visits, 568 history::URLRow* new_url, 569 std::vector<history::VisitInfo>* new_visits) { 570 DCHECK(new_url); 571 DCHECK(!node.url().compare(url.url().spec())); 572 DCHECK(!node.url().compare(new_url->url().spec())); 573 DCHECK(visits->size()); 574 CHECK_EQ(node.visits_size(), node.visit_transitions_size()); 575 576 // If we have an old-format node (before we added the visits and 577 // visit_transitions arrays to the protobuf) or else the node only contained 578 // expired visits, so just overwrite it with our local history data. 579 if (node.visits_size() == 0) 580 return DIFF_UPDATE_NODE; 581 582 // Convert these values only once. 583 base::string16 node_title(base::UTF8ToUTF16(node.title())); 584 base::Time node_last_visit = base::Time::FromInternalValue( 585 node.visits(node.visits_size() - 1)); 586 587 // This is a bitfield representing what we'll need to update with the output 588 // value. 589 MergeResult different = DIFF_NONE; 590 591 // Check if the non-incremented values changed. 592 if ((node_title.compare(url.title()) != 0) || 593 (node.hidden() != url.hidden())) { 594 // Use the values from the most recent visit. 595 if (node_last_visit >= url.last_visit()) { 596 new_url->set_title(node_title); 597 new_url->set_hidden(node.hidden()); 598 different |= DIFF_LOCAL_ROW_CHANGED; 599 } else { 600 new_url->set_title(url.title()); 601 new_url->set_hidden(url.hidden()); 602 different |= DIFF_UPDATE_NODE; 603 } 604 } else { 605 // No difference. 606 new_url->set_title(url.title()); 607 new_url->set_hidden(url.hidden()); 608 } 609 610 size_t node_num_visits = node.visits_size(); 611 size_t history_num_visits = visits->size(); 612 size_t node_visit_index = 0; 613 size_t history_visit_index = 0; 614 base::Time earliest_history_time = (*visits)[0].visit_time; 615 // Walk through the two sets of visits and figure out if any new visits were 616 // added on either side. 617 while (node_visit_index < node_num_visits || 618 history_visit_index < history_num_visits) { 619 // Time objects are initialized to "earliest possible time". 620 base::Time node_time, history_time; 621 if (node_visit_index < node_num_visits) 622 node_time = base::Time::FromInternalValue(node.visits(node_visit_index)); 623 if (history_visit_index < history_num_visits) 624 history_time = (*visits)[history_visit_index].visit_time; 625 if (node_visit_index >= node_num_visits || 626 (history_visit_index < history_num_visits && 627 node_time > history_time)) { 628 // We found a visit in the history DB that doesn't exist in the sync DB, 629 // so mark the node as modified so the caller will update the sync node. 630 different |= DIFF_UPDATE_NODE; 631 ++history_visit_index; 632 } else if (history_visit_index >= history_num_visits || 633 node_time < history_time) { 634 // Found a visit in the sync node that doesn't exist in the history DB, so 635 // add it to our list of new visits and set the appropriate flag so the 636 // caller will update the history DB. 637 // If the node visit is older than any existing visit in the history DB, 638 // don't re-add it - this keeps us from resurrecting visits that were 639 // aged out locally. 640 if (node_time > earliest_history_time) { 641 different |= DIFF_LOCAL_VISITS_ADDED; 642 new_visits->push_back(history::VisitInfo( 643 node_time, 644 content::PageTransitionFromInt( 645 node.visit_transitions(node_visit_index)))); 646 } 647 // This visit is added to visits below. 648 ++node_visit_index; 649 } else { 650 // Same (already synced) entry found in both DBs - no need to do anything. 651 ++node_visit_index; 652 ++history_visit_index; 653 } 654 } 655 656 DCHECK(CheckVisitOrdering(*visits)); 657 if (different & DIFF_LOCAL_VISITS_ADDED) { 658 // Insert new visits into the apropriate place in the visits vector. 659 history::VisitVector::iterator visit_ix = visits->begin(); 660 for (std::vector<history::VisitInfo>::iterator new_visit = 661 new_visits->begin(); 662 new_visit != new_visits->end(); ++new_visit) { 663 while (visit_ix != visits->end() && 664 new_visit->first > visit_ix->visit_time) { 665 ++visit_ix; 666 } 667 visit_ix = visits->insert(visit_ix, 668 history::VisitRow(url.id(), new_visit->first, 669 0, new_visit->second, 0)); 670 ++visit_ix; 671 } 672 } 673 DCHECK(CheckVisitOrdering(*visits)); 674 675 new_url->set_last_visit(visits->back().visit_time); 676 return different; 677 } 678 679 // static 680 void TypedUrlModelAssociator::WriteToSyncNode( 681 const history::URLRow& url, 682 const history::VisitVector& visits, 683 syncer::WriteNode* node) { 684 sync_pb::TypedUrlSpecifics typed_url; 685 WriteToTypedUrlSpecifics(url, visits, &typed_url); 686 node->SetTypedUrlSpecifics(typed_url); 687 } 688 689 void TypedUrlModelAssociator::WriteToTypedUrlSpecifics( 690 const history::URLRow& url, 691 const history::VisitVector& visits, 692 sync_pb::TypedUrlSpecifics* typed_url) { 693 694 DCHECK(!url.last_visit().is_null()); 695 DCHECK(!visits.empty()); 696 DCHECK_EQ(url.last_visit().ToInternalValue(), 697 visits.back().visit_time.ToInternalValue()); 698 699 typed_url->set_url(url.url().spec()); 700 typed_url->set_title(base::UTF16ToUTF8(url.title())); 701 typed_url->set_hidden(url.hidden()); 702 703 DCHECK(CheckVisitOrdering(visits)); 704 705 bool only_typed = false; 706 int skip_count = 0; 707 708 if (visits.size() > static_cast<size_t>(kMaxTypedUrlVisits)) { 709 int typed_count = 0; 710 int total = 0; 711 // Walk the passed-in visit vector and count the # of typed visits. 712 for (history::VisitVector::const_iterator visit = visits.begin(); 713 visit != visits.end(); ++visit) { 714 content::PageTransition transition = content::PageTransitionFromInt( 715 visit->transition & content::PAGE_TRANSITION_CORE_MASK); 716 // We ignore reload visits. 717 if (transition == content::PAGE_TRANSITION_RELOAD) 718 continue; 719 ++total; 720 if (transition == content::PAGE_TRANSITION_TYPED) 721 ++typed_count; 722 } 723 // We should have at least one typed visit. This can sometimes happen if 724 // the history DB has an inaccurate count for some reason (there's been 725 // bugs in the history code in the past which has left users in the wild 726 // with incorrect counts - http://crbug.com/84258). 727 DCHECK(typed_count > 0); 728 729 if (typed_count > kMaxTypedUrlVisits) { 730 only_typed = true; 731 skip_count = typed_count - kMaxTypedUrlVisits; 732 } else if (total > kMaxTypedUrlVisits) { 733 skip_count = total - kMaxTypedUrlVisits; 734 } 735 } 736 737 738 for (history::VisitVector::const_iterator visit = visits.begin(); 739 visit != visits.end(); ++visit) { 740 content::PageTransition transition = content::PageTransitionFromInt( 741 visit->transition & content::PAGE_TRANSITION_CORE_MASK); 742 // Skip reload visits. 743 if (transition == content::PAGE_TRANSITION_RELOAD) 744 continue; 745 746 // If we only have room for typed visits, then only add typed visits. 747 if (only_typed && transition != content::PAGE_TRANSITION_TYPED) 748 continue; 749 750 if (skip_count > 0) { 751 // We have too many entries to fit, so we need to skip the oldest ones. 752 // Only skip typed URLs if there are too many typed URLs to fit. 753 if (only_typed || transition != content::PAGE_TRANSITION_TYPED) { 754 --skip_count; 755 continue; 756 } 757 } 758 typed_url->add_visits(visit->visit_time.ToInternalValue()); 759 typed_url->add_visit_transitions(visit->transition); 760 } 761 DCHECK_EQ(skip_count, 0); 762 763 if (typed_url->visits_size() == 0) { 764 // If we get here, it's because we don't actually have any TYPED visits 765 // even though the visit's typed_count > 0 (corrupted typed_count). So 766 // let's go ahead and add a RELOAD visit at the most recent visit since 767 // it's not legal to have an empty visit array (yet another workaround 768 // for http://crbug.com/84258). 769 typed_url->add_visits(url.last_visit().ToInternalValue()); 770 typed_url->add_visit_transitions(content::PAGE_TRANSITION_RELOAD); 771 } 772 CHECK_GT(typed_url->visits_size(), 0); 773 CHECK_LE(typed_url->visits_size(), kMaxTypedUrlVisits); 774 CHECK_EQ(typed_url->visits_size(), typed_url->visit_transitions_size()); 775 } 776 777 // static 778 void TypedUrlModelAssociator::DiffVisits( 779 const history::VisitVector& old_visits, 780 const sync_pb::TypedUrlSpecifics& new_url, 781 std::vector<history::VisitInfo>* new_visits, 782 history::VisitVector* removed_visits) { 783 DCHECK(new_visits); 784 size_t old_visit_count = old_visits.size(); 785 size_t new_visit_count = new_url.visits_size(); 786 size_t old_index = 0; 787 size_t new_index = 0; 788 while (old_index < old_visit_count && new_index < new_visit_count) { 789 base::Time new_visit_time = 790 base::Time::FromInternalValue(new_url.visits(new_index)); 791 if (old_visits[old_index].visit_time < new_visit_time) { 792 if (new_index > 0 && removed_visits) { 793 // If there are visits missing from the start of the node, that 794 // means that they were probably clipped off due to our code that 795 // limits the size of the sync nodes - don't delete them from our 796 // local history. 797 removed_visits->push_back(old_visits[old_index]); 798 } 799 ++old_index; 800 } else if (old_visits[old_index].visit_time > new_visit_time) { 801 new_visits->push_back(history::VisitInfo( 802 new_visit_time, 803 content::PageTransitionFromInt( 804 new_url.visit_transitions(new_index)))); 805 ++new_index; 806 } else { 807 ++old_index; 808 ++new_index; 809 } 810 } 811 812 if (removed_visits) { 813 for ( ; old_index < old_visit_count; ++old_index) { 814 removed_visits->push_back(old_visits[old_index]); 815 } 816 } 817 818 for ( ; new_index < new_visit_count; ++new_index) { 819 new_visits->push_back(history::VisitInfo( 820 base::Time::FromInternalValue(new_url.visits(new_index)), 821 content::PageTransitionFromInt(new_url.visit_transitions(new_index)))); 822 } 823 } 824 825 826 // static 827 void TypedUrlModelAssociator::UpdateURLRowFromTypedUrlSpecifics( 828 const sync_pb::TypedUrlSpecifics& typed_url, history::URLRow* new_url) { 829 DCHECK_GT(typed_url.visits_size(), 0); 830 CHECK_EQ(typed_url.visit_transitions_size(), typed_url.visits_size()); 831 new_url->set_title(base::UTF8ToUTF16(typed_url.title())); 832 new_url->set_hidden(typed_url.hidden()); 833 // Only provide the initial value for the last_visit field - after that, let 834 // the history code update the last_visit field on its own. 835 if (new_url->last_visit().is_null()) { 836 new_url->set_last_visit(base::Time::FromInternalValue( 837 typed_url.visits(typed_url.visits_size() - 1))); 838 } 839 } 840 841 bool TypedUrlModelAssociator::CryptoReadyIfNecessary() { 842 // We only access the cryptographer while holding a transaction. 843 syncer::ReadTransaction trans(FROM_HERE, sync_service_->GetUserShare()); 844 const syncer::ModelTypeSet encrypted_types = trans.GetEncryptedTypes(); 845 return !encrypted_types.Has(syncer::TYPED_URLS) || 846 sync_service_->IsCryptographerReady(&trans); 847 } 848 849 } // namespace browser_sync 850