Home | History | Annotate | Download | only in glue
      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 
     25 using content::BrowserThread;
     26 
     27 namespace 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.
     31 static 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).
     35 static const int kTypedUrlVisitThrottleMultiple = 10;
     36 
     37 TypedUrlChangeProcessor::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 
     58 TypedUrlChangeProcessor::~TypedUrlChangeProcessor() {
     59   DCHECK(backend_loop_ == base::MessageLoop::current());
     60 }
     61 
     62 void 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 
     88 void 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 
    102 bool 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 
    182 void 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 
    215 void 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 
    224 bool 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 
    243 void 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 
    306 void 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 
    333 void TypedUrlChangeProcessor::Disconnect() {
    334   base::AutoLock al(disconnect_lock_);
    335   disconnected_ = true;
    336 }
    337 
    338 void 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 
    348 void 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 
    362 void 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