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