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