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_change_processor.h"
6
7#include "base/location.h"
8#include "base/metrics/histogram.h"
9#include "base/strings/string_util.h"
10#include "base/strings/utf_string_conversions.h"
11#include "chrome/browser/chrome_notification_types.h"
12#include "chrome/browser/history/history_backend.h"
13#include "chrome/browser/history/history_notifications.h"
14#include "chrome/browser/profiles/profile.h"
15#include "chrome/browser/sync/glue/typed_url_model_associator.h"
16#include "chrome/browser/sync/profile_sync_service.h"
17#include "content/public/browser/notification_service.h"
18#include "sync/internal_api/public/change_record.h"
19#include "sync/internal_api/public/read_node.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#include "sync/syncable/entry.h"  // TODO(tim): Investigating bug 121587.
24
25using content::BrowserThread;
26
27namespace browser_sync {
28
29// This is the threshold at which we start throttling sync updates for typed
30// URLs - any URLs with a typed_count >= this threshold will be throttled.
31static const int kTypedUrlVisitThrottleThreshold = 10;
32
33// This is the multiple we use when throttling sync updates. If the multiple is
34// N, we sync up every Nth update (i.e. when typed_count % N == 0).
35static const int kTypedUrlVisitThrottleMultiple = 10;
36
37TypedUrlChangeProcessor::TypedUrlChangeProcessor(
38    Profile* profile,
39    TypedUrlModelAssociator* model_associator,
40    history::HistoryBackend* history_backend,
41    DataTypeErrorHandler* error_handler)
42    : ChangeProcessor(error_handler),
43      profile_(profile),
44      model_associator_(model_associator),
45      history_backend_(history_backend),
46      backend_loop_(base::MessageLoop::current()),
47      disconnected_(false) {
48  DCHECK(model_associator);
49  DCHECK(history_backend);
50  DCHECK(error_handler);
51  DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::UI));
52  // When running in unit tests, there is already a NotificationService object.
53  // Since only one can exist at a time per thread, check first.
54  if (!content::NotificationService::current())
55    notification_service_.reset(content::NotificationService::Create());
56}
57
58TypedUrlChangeProcessor::~TypedUrlChangeProcessor() {
59  DCHECK(backend_loop_ == base::MessageLoop::current());
60}
61
62void TypedUrlChangeProcessor::Observe(
63    int type,
64    const content::NotificationSource& source,
65    const content::NotificationDetails& details) {
66  DCHECK(backend_loop_ == base::MessageLoop::current());
67
68  base::AutoLock al(disconnect_lock_);
69  if (disconnected_)
70    return;
71
72  DVLOG(1) << "Observed typed_url change.";
73  if (type == chrome::NOTIFICATION_HISTORY_URLS_MODIFIED) {
74    HandleURLsModified(
75        content::Details<history::URLsModifiedDetails>(details).ptr());
76  } else if (type == chrome::NOTIFICATION_HISTORY_URLS_DELETED) {
77    HandleURLsDeleted(
78        content::Details<history::URLsDeletedDetails>(details).ptr());
79  } else {
80    DCHECK_EQ(chrome::NOTIFICATION_HISTORY_URL_VISITED, type);
81    HandleURLsVisited(
82        content::Details<history::URLVisitedDetails>(details).ptr());
83  }
84  UMA_HISTOGRAM_PERCENTAGE("Sync.TypedUrlChangeProcessorErrors",
85                           model_associator_->GetErrorPercentage());
86}
87
88void TypedUrlChangeProcessor::HandleURLsModified(
89    history::URLsModifiedDetails* details) {
90
91  syncer::WriteTransaction trans(FROM_HERE, share_handle());
92  for (history::URLRows::iterator url = details->changed_urls.begin();
93       url != details->changed_urls.end(); ++url) {
94    if (url->typed_count() > 0) {
95      // If there were any errors updating the sync node, just ignore them and
96      // continue on to process the next URL.
97      CreateOrUpdateSyncNode(*url, &trans);
98    }
99  }
100}
101
102bool TypedUrlChangeProcessor::CreateOrUpdateSyncNode(
103    history::URLRow url, syncer::WriteTransaction* trans) {
104  DCHECK_GT(url.typed_count(), 0);
105  // Get the visits for this node.
106  history::VisitVector visit_vector;
107  if (!model_associator_->FixupURLAndGetVisits(&url, &visit_vector)) {
108    DLOG(ERROR) << "Could not load visits for url: " << url.url();
109    return false;
110  }
111
112  syncer::ReadNode typed_url_root(trans);
113  if (typed_url_root.InitByTagLookup(kTypedUrlTag) !=
114          syncer::BaseNode::INIT_OK) {
115    error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE,
116        "Server did not create the top-level typed_url node. We "
117         "might be running against an out-of-date server.");
118    return false;
119  }
120
121  if (model_associator_->ShouldIgnoreUrl(url.url()))
122    return true;
123
124  DCHECK(!visit_vector.empty());
125  std::string tag = url.url().spec();
126  syncer::WriteNode update_node(trans);
127  syncer::BaseNode::InitByLookupResult result =
128      update_node.InitByClientTagLookup(syncer::TYPED_URLS, tag);
129  if (result == syncer::BaseNode::INIT_OK) {
130    model_associator_->WriteToSyncNode(url, visit_vector, &update_node);
131  } else if (result == syncer::BaseNode::INIT_FAILED_DECRYPT_IF_NECESSARY) {
132    // TODO(tim): Investigating bug 121587.
133    syncer::Cryptographer* crypto = trans->GetCryptographer();
134    syncer::ModelTypeSet encrypted_types(trans->GetEncryptedTypes());
135    const sync_pb::EntitySpecifics& specifics =
136        update_node.GetEntry()->Get(syncer::syncable::SPECIFICS);
137    CHECK(specifics.has_encrypted());
138    const bool can_decrypt = crypto->CanDecrypt(specifics.encrypted());
139    const bool agreement = encrypted_types.Has(syncer::TYPED_URLS);
140    if (!agreement && !can_decrypt) {
141      error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE,
142          "Could not InitByIdLookup in CreateOrUpdateSyncNode, "
143          " Cryptographer thinks typed urls not encrypted, and CanDecrypt"
144          " failed.");
145      LOG(ERROR) << "Case 1.";
146    } else if (agreement && can_decrypt) {
147      error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE,
148          "Could not InitByIdLookup on CreateOrUpdateSyncNode, "
149          " Cryptographer thinks typed urls are encrypted, and CanDecrypt"
150          " succeeded (?!), but DecryptIfNecessary failed.");
151      LOG(ERROR) << "Case 2.";
152    } else if (agreement) {
153      error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE,
154          "Could not InitByIdLookup on CreateOrUpdateSyncNode, "
155          " Cryptographer thinks typed urls are encrypted, but CanDecrypt"
156          " failed.");
157      LOG(ERROR) << "Case 3.";
158    } else {
159      error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE,
160          "Could not InitByIdLookup on CreateOrUpdateSyncNode, "
161          " Cryptographer thinks typed urls not encrypted, but CanDecrypt"
162          " succeeded (super weird, btw)");
163      LOG(ERROR) << "Case 4.";
164    }
165  } else {
166    syncer::WriteNode create_node(trans);
167    syncer::WriteNode::InitUniqueByCreationResult result =
168        create_node.InitUniqueByCreation(syncer::TYPED_URLS,
169                                         typed_url_root, tag);
170    if (result != syncer::WriteNode::INIT_SUCCESS) {
171      error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE,
172          "Failed to create typed_url sync node.");
173      return false;
174    }
175
176    create_node.SetTitle(UTF8ToWide(tag));
177    model_associator_->WriteToSyncNode(url, visit_vector, &create_node);
178  }
179  return true;
180}
181
182void TypedUrlChangeProcessor::HandleURLsDeleted(
183    history::URLsDeletedDetails* details) {
184  syncer::WriteTransaction trans(FROM_HERE, share_handle());
185
186  // Ignore archivals (we don't want to sync them as deletions, to avoid
187  // extra traffic up to the server, and also to make sure that a client with
188  // a bad clock setting won't go on an archival rampage and delete all
189  // history from every client). The server will gracefully age out the sync DB
190  // entries when they've been idle for long enough.
191  if (details->archived)
192    return;
193
194  if (details->all_history) {
195    if (!model_associator_->DeleteAllNodes(&trans)) {
196      error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE,
197          std::string());
198      return;
199    }
200  } else {
201    for (history::URLRows::const_iterator row = details->rows.begin();
202         row != details->rows.end(); ++row) {
203      syncer::WriteNode sync_node(&trans);
204      // The deleted URL could have been non-typed, so it might not be found
205      // in the sync DB.
206      if (sync_node.InitByClientTagLookup(syncer::TYPED_URLS,
207                                          row->url().spec()) ==
208              syncer::BaseNode::INIT_OK) {
209        sync_node.Tombstone();
210      }
211    }
212  }
213}
214
215void TypedUrlChangeProcessor::HandleURLsVisited(
216    history::URLVisitedDetails* details) {
217  if (!ShouldSyncVisit(details))
218    return;
219
220  syncer::WriteTransaction trans(FROM_HERE, share_handle());
221  CreateOrUpdateSyncNode(details->row, &trans);
222}
223
224bool TypedUrlChangeProcessor::ShouldSyncVisit(
225    history::URLVisitedDetails* details) {
226  int typed_count = details->row.typed_count();
227  content::PageTransition transition = static_cast<content::PageTransition>(
228      details->transition & content::PAGE_TRANSITION_CORE_MASK);
229
230  // Just use an ad-hoc criteria to determine whether to ignore this
231  // notification. For most users, the distribution of visits is roughly a bell
232  // curve with a long tail - there are lots of URLs with < 5 visits so we want
233  // to make sure we sync up every visit to ensure the proper ordering of
234  // suggestions. But there are relatively few URLs with > 10 visits, and those
235  // tend to be more broadly distributed such that there's no need to sync up
236  // every visit to preserve their relative ordering.
237  return (transition == content::PAGE_TRANSITION_TYPED &&
238          typed_count > 0 &&
239          (typed_count < kTypedUrlVisitThrottleThreshold ||
240           (typed_count % kTypedUrlVisitThrottleMultiple) == 0));
241}
242
243void TypedUrlChangeProcessor::ApplyChangesFromSyncModel(
244    const syncer::BaseTransaction* trans,
245    int64 model_version,
246    const syncer::ImmutableChangeRecordList& changes) {
247  DCHECK(backend_loop_ == base::MessageLoop::current());
248
249  base::AutoLock al(disconnect_lock_);
250  if (disconnected_)
251    return;
252
253  syncer::ReadNode typed_url_root(trans);
254  if (typed_url_root.InitByTagLookup(kTypedUrlTag) !=
255          syncer::BaseNode::INIT_OK) {
256    error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE,
257        "TypedUrl root node lookup failed.");
258    return;
259  }
260
261  DCHECK(pending_new_urls_.empty() && pending_new_visits_.empty() &&
262         pending_deleted_visits_.empty() && pending_updated_urls_.empty() &&
263         pending_deleted_urls_.empty());
264
265  for (syncer::ChangeRecordList::const_iterator it =
266           changes.Get().begin(); it != changes.Get().end(); ++it) {
267    if (syncer::ChangeRecord::ACTION_DELETE ==
268        it->action) {
269      DCHECK(it->specifics.has_typed_url()) <<
270          "Typed URL delete change does not have necessary specifics.";
271      GURL url(it->specifics.typed_url().url());
272      pending_deleted_urls_.push_back(url);
273      continue;
274    }
275
276    syncer::ReadNode sync_node(trans);
277    if (sync_node.InitByIdLookup(it->id) != syncer::BaseNode::INIT_OK) {
278      error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE,
279          "TypedUrl node lookup failed.");
280      return;
281    }
282
283    // Check that the changed node is a child of the typed_urls folder.
284    DCHECK(typed_url_root.GetId() == sync_node.GetParentId());
285    DCHECK(syncer::TYPED_URLS == sync_node.GetModelType());
286
287    const sync_pb::TypedUrlSpecifics& typed_url(
288        sync_node.GetTypedUrlSpecifics());
289    DCHECK(typed_url.visits_size());
290
291    if (model_associator_->ShouldIgnoreUrl(GURL(typed_url.url())))
292      continue;
293
294    sync_pb::TypedUrlSpecifics filtered_url =
295        model_associator_->FilterExpiredVisits(typed_url);
296    if (!filtered_url.visits_size()) {
297      continue;
298    }
299
300    model_associator_->UpdateFromSyncDB(
301        filtered_url, &pending_new_visits_, &pending_deleted_visits_,
302        &pending_updated_urls_, &pending_new_urls_);
303  }
304}
305
306void TypedUrlChangeProcessor::CommitChangesFromSyncModel() {
307  DCHECK(backend_loop_ == base::MessageLoop::current());
308
309  base::AutoLock al(disconnect_lock_);
310  if (disconnected_)
311    return;
312
313  // Make sure we stop listening for changes while we're modifying the backend,
314  // so we don't try to re-apply these changes to the sync DB.
315  ScopedStopObserving<TypedUrlChangeProcessor> stop_observing(this);
316  if (!pending_deleted_urls_.empty())
317    history_backend_->DeleteURLs(pending_deleted_urls_);
318
319  model_associator_->WriteToHistoryBackend(&pending_new_urls_,
320                                           &pending_updated_urls_,
321                                           &pending_new_visits_,
322                                           &pending_deleted_visits_);
323
324  pending_new_urls_.clear();
325  pending_updated_urls_.clear();
326  pending_new_visits_.clear();
327  pending_deleted_visits_.clear();
328  pending_deleted_urls_.clear();
329  UMA_HISTOGRAM_PERCENTAGE("Sync.TypedUrlChangeProcessorErrors",
330                           model_associator_->GetErrorPercentage());
331}
332
333void TypedUrlChangeProcessor::Disconnect() {
334  base::AutoLock al(disconnect_lock_);
335  disconnected_ = true;
336}
337
338void TypedUrlChangeProcessor::StartImpl(Profile* profile) {
339  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
340  DCHECK_EQ(profile, profile_);
341  DCHECK(history_backend_);
342  DCHECK(backend_loop_);
343  backend_loop_->PostTask(FROM_HERE,
344                          base::Bind(&TypedUrlChangeProcessor::StartObserving,
345                                     base::Unretained(this)));
346}
347
348void TypedUrlChangeProcessor::StartObserving() {
349  DCHECK(backend_loop_ == base::MessageLoop::current());
350  DCHECK(profile_);
351  notification_registrar_.Add(
352      this, chrome::NOTIFICATION_HISTORY_URLS_MODIFIED,
353      content::Source<Profile>(profile_));
354  notification_registrar_.Add(
355      this, chrome::NOTIFICATION_HISTORY_URLS_DELETED,
356      content::Source<Profile>(profile_));
357  notification_registrar_.Add(
358      this, chrome::NOTIFICATION_HISTORY_URL_VISITED,
359      content::Source<Profile>(profile_));
360}
361
362void TypedUrlChangeProcessor::StopObserving() {
363  DCHECK(backend_loop_ == base::MessageLoop::current());
364  DCHECK(profile_);
365  notification_registrar_.Remove(
366      this, chrome::NOTIFICATION_HISTORY_URLS_MODIFIED,
367      content::Source<Profile>(profile_));
368  notification_registrar_.Remove(
369      this, chrome::NOTIFICATION_HISTORY_URLS_DELETED,
370      content::Source<Profile>(profile_));
371  notification_registrar_.Remove(
372      this, chrome::NOTIFICATION_HISTORY_URL_VISITED,
373      content::Source<Profile>(profile_));
374}
375
376}  // namespace browser_sync
377