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