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