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 DataTypeErrorHandler* error_handler) 43 : 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 error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE, 117 "Server did not create the top-level typed_url node. We " 118 "might be running against an out-of-date server."); 119 return false; 120 } 121 122 if (model_associator_->ShouldIgnoreUrl(url.url())) 123 return true; 124 125 DCHECK(!visit_vector.empty()); 126 std::string tag = url.url().spec(); 127 syncer::WriteNode update_node(trans); 128 syncer::BaseNode::InitByLookupResult result = 129 update_node.InitByClientTagLookup(syncer::TYPED_URLS, tag); 130 if (result == syncer::BaseNode::INIT_OK) { 131 model_associator_->WriteToSyncNode(url, visit_vector, &update_node); 132 } else if (result == syncer::BaseNode::INIT_FAILED_DECRYPT_IF_NECESSARY) { 133 // TODO(tim): Investigating bug 121587. 134 syncer::Cryptographer* crypto = trans->GetCryptographer(); 135 syncer::ModelTypeSet encrypted_types(trans->GetEncryptedTypes()); 136 const sync_pb::EntitySpecifics& specifics = 137 update_node.GetEntry()->GetSpecifics(); 138 CHECK(specifics.has_encrypted()); 139 const bool can_decrypt = crypto->CanDecrypt(specifics.encrypted()); 140 const bool agreement = encrypted_types.Has(syncer::TYPED_URLS); 141 if (!agreement && !can_decrypt) { 142 error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE, 143 "Could not InitByIdLookup in CreateOrUpdateSyncNode, " 144 " Cryptographer thinks typed urls not encrypted, and CanDecrypt" 145 " failed."); 146 LOG(ERROR) << "Case 1."; 147 } else if (agreement && can_decrypt) { 148 error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE, 149 "Could not InitByIdLookup on CreateOrUpdateSyncNode, " 150 " Cryptographer thinks typed urls are encrypted, and CanDecrypt" 151 " succeeded (?!), but DecryptIfNecessary failed."); 152 LOG(ERROR) << "Case 2."; 153 } else if (agreement) { 154 error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE, 155 "Could not InitByIdLookup on CreateOrUpdateSyncNode, " 156 " Cryptographer thinks typed urls are encrypted, but CanDecrypt" 157 " failed."); 158 LOG(ERROR) << "Case 3."; 159 } else { 160 error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE, 161 "Could not InitByIdLookup on CreateOrUpdateSyncNode, " 162 " Cryptographer thinks typed urls not encrypted, but CanDecrypt" 163 " succeeded (super weird, btw)"); 164 LOG(ERROR) << "Case 4."; 165 } 166 } else { 167 syncer::WriteNode create_node(trans); 168 syncer::WriteNode::InitUniqueByCreationResult result = 169 create_node.InitUniqueByCreation(syncer::TYPED_URLS, 170 typed_url_root, tag); 171 if (result != syncer::WriteNode::INIT_SUCCESS) { 172 error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE, 173 "Failed to create typed_url sync node."); 174 return false; 175 } 176 177 create_node.SetTitle(tag); 178 model_associator_->WriteToSyncNode(url, visit_vector, &create_node); 179 } 180 return true; 181} 182 183void TypedUrlChangeProcessor::HandleURLsDeleted( 184 history::URLsDeletedDetails* details) { 185 syncer::WriteTransaction trans(FROM_HERE, share_handle()); 186 187 // Ignore archivals (we don't want to sync them as deletions, to avoid 188 // extra traffic up to the server, and also to make sure that a client with 189 // a bad clock setting won't go on an archival rampage and delete all 190 // history from every client). The server will gracefully age out the sync DB 191 // entries when they've been idle for long enough. 192 if (details->expired) 193 return; 194 195 if (details->all_history) { 196 if (!model_associator_->DeleteAllNodes(&trans)) { 197 error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE, 198 std::string()); 199 return; 200 } 201 } else { 202 for (history::URLRows::const_iterator row = details->rows.begin(); 203 row != details->rows.end(); ++row) { 204 syncer::WriteNode sync_node(&trans); 205 // The deleted URL could have been non-typed, so it might not be found 206 // in the sync DB. 207 if (sync_node.InitByClientTagLookup(syncer::TYPED_URLS, 208 row->url().spec()) == 209 syncer::BaseNode::INIT_OK) { 210 sync_node.Tombstone(); 211 } 212 } 213 } 214} 215 216void TypedUrlChangeProcessor::HandleURLsVisited( 217 history::URLVisitedDetails* details) { 218 if (!ShouldSyncVisit(details)) 219 return; 220 221 syncer::WriteTransaction trans(FROM_HERE, share_handle()); 222 CreateOrUpdateSyncNode(details->row, &trans); 223} 224 225bool TypedUrlChangeProcessor::ShouldSyncVisit( 226 history::URLVisitedDetails* details) { 227 int typed_count = details->row.typed_count(); 228 content::PageTransition transition = static_cast<content::PageTransition>( 229 details->transition & content::PAGE_TRANSITION_CORE_MASK); 230 231 // Just use an ad-hoc criteria to determine whether to ignore this 232 // notification. For most users, the distribution of visits is roughly a bell 233 // curve with a long tail - there are lots of URLs with < 5 visits so we want 234 // to make sure we sync up every visit to ensure the proper ordering of 235 // suggestions. But there are relatively few URLs with > 10 visits, and those 236 // tend to be more broadly distributed such that there's no need to sync up 237 // every visit to preserve their relative ordering. 238 return (transition == content::PAGE_TRANSITION_TYPED && 239 typed_count > 0 && 240 (typed_count < kTypedUrlVisitThrottleThreshold || 241 (typed_count % kTypedUrlVisitThrottleMultiple) == 0)); 242} 243 244void TypedUrlChangeProcessor::ApplyChangesFromSyncModel( 245 const syncer::BaseTransaction* trans, 246 int64 model_version, 247 const syncer::ImmutableChangeRecordList& changes) { 248 DCHECK(backend_loop_ == base::MessageLoop::current()); 249 250 base::AutoLock al(disconnect_lock_); 251 if (disconnected_) 252 return; 253 254 syncer::ReadNode typed_url_root(trans); 255 if (typed_url_root.InitTypeRoot(syncer::TYPED_URLS) != 256 syncer::BaseNode::INIT_OK) { 257 error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE, 258 "TypedUrl root node lookup failed."); 259 return; 260 } 261 262 DCHECK(pending_new_urls_.empty() && pending_new_visits_.empty() && 263 pending_deleted_visits_.empty() && pending_updated_urls_.empty() && 264 pending_deleted_urls_.empty()); 265 266 for (syncer::ChangeRecordList::const_iterator it = 267 changes.Get().begin(); it != changes.Get().end(); ++it) { 268 if (syncer::ChangeRecord::ACTION_DELETE == 269 it->action) { 270 DCHECK(it->specifics.has_typed_url()) << 271 "Typed URL delete change does not have necessary specifics."; 272 GURL url(it->specifics.typed_url().url()); 273 pending_deleted_urls_.push_back(url); 274 continue; 275 } 276 277 syncer::ReadNode sync_node(trans); 278 if (sync_node.InitByIdLookup(it->id) != syncer::BaseNode::INIT_OK) { 279 error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE, 280 "TypedUrl node lookup failed."); 281 return; 282 } 283 284 // Check that the changed node is a child of the typed_urls folder. 285 DCHECK(typed_url_root.GetId() == sync_node.GetParentId()); 286 DCHECK(syncer::TYPED_URLS == sync_node.GetModelType()); 287 288 const sync_pb::TypedUrlSpecifics& typed_url( 289 sync_node.GetTypedUrlSpecifics()); 290 DCHECK(typed_url.visits_size()); 291 292 if (model_associator_->ShouldIgnoreUrl(GURL(typed_url.url()))) 293 continue; 294 295 sync_pb::TypedUrlSpecifics filtered_url = 296 model_associator_->FilterExpiredVisits(typed_url); 297 if (!filtered_url.visits_size()) { 298 continue; 299 } 300 301 model_associator_->UpdateFromSyncDB( 302 filtered_url, &pending_new_visits_, &pending_deleted_visits_, 303 &pending_updated_urls_, &pending_new_urls_); 304 } 305} 306 307void TypedUrlChangeProcessor::CommitChangesFromSyncModel() { 308 DCHECK(backend_loop_ == base::MessageLoop::current()); 309 310 base::AutoLock al(disconnect_lock_); 311 if (disconnected_) 312 return; 313 314 // Make sure we stop listening for changes while we're modifying the backend, 315 // so we don't try to re-apply these changes to the sync DB. 316 ScopedStopObserving<TypedUrlChangeProcessor> stop_observing(this); 317 if (!pending_deleted_urls_.empty()) 318 history_backend_->DeleteURLs(pending_deleted_urls_); 319 320 model_associator_->WriteToHistoryBackend(&pending_new_urls_, 321 &pending_updated_urls_, 322 &pending_new_visits_, 323 &pending_deleted_visits_); 324 325 pending_new_urls_.clear(); 326 pending_updated_urls_.clear(); 327 pending_new_visits_.clear(); 328 pending_deleted_visits_.clear(); 329 pending_deleted_urls_.clear(); 330 UMA_HISTOGRAM_PERCENTAGE("Sync.TypedUrlChangeProcessorErrors", 331 model_associator_->GetErrorPercentage()); 332} 333 334void TypedUrlChangeProcessor::Disconnect() { 335 base::AutoLock al(disconnect_lock_); 336 disconnected_ = true; 337} 338 339void TypedUrlChangeProcessor::StartImpl() { 340 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 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