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/session_change_processor.h" 6 7 #include <string> 8 #include <vector> 9 10 #include "base/logging.h" 11 #include "chrome/browser/chrome_notification_types.h" 12 #include "chrome/browser/extensions/tab_helper.h" 13 #include "chrome/browser/favicon/favicon_changed_details.h" 14 #include "chrome/browser/profiles/profile.h" 15 #include "chrome/browser/sync/glue/session_model_associator.h" 16 #include "chrome/browser/sync/glue/synced_tab_delegate.h" 17 #include "chrome/browser/sync/profile_sync_service.h" 18 #include "content/public/browser/navigation_controller.h" 19 #include "content/public/browser/navigation_entry.h" 20 #include "content/public/browser/notification_details.h" 21 #include "content/public/browser/notification_service.h" 22 #include "content/public/browser/notification_source.h" 23 #include "content/public/browser/web_contents.h" 24 #include "sync/api/sync_error.h" 25 #include "sync/internal_api/public/base/model_type.h" 26 #include "sync/internal_api/public/base/model_type_invalidation_map.h" 27 #include "sync/internal_api/public/change_record.h" 28 #include "sync/internal_api/public/read_node.h" 29 #include "sync/protocol/session_specifics.pb.h" 30 31 #if defined(ENABLE_MANAGED_USERS) 32 #include "chrome/browser/managed_mode/managed_user_service.h" 33 #include "chrome/browser/managed_mode/managed_user_service_factory.h" 34 #endif 35 36 using content::BrowserThread; 37 using content::NavigationController; 38 using content::WebContents; 39 40 namespace browser_sync { 41 42 namespace { 43 44 // The URL at which the set of synced tabs is displayed. We treat it differently 45 // from all other URL's as accessing it triggers a sync refresh of Sessions. 46 static const char kNTPOpenTabSyncURL[] = "chrome://newtab/#open_tabs"; 47 48 // Extract the source SyncedTabDelegate from a NotificationSource originating 49 // from a NavigationController, if it exists. Returns |NULL| otherwise. 50 SyncedTabDelegate* ExtractSyncedTabDelegate( 51 const content::NotificationSource& source) { 52 return SyncedTabDelegate::ImplFromWebContents( 53 content::Source<NavigationController>(source).ptr()->GetWebContents()); 54 } 55 56 } // namespace 57 58 SessionChangeProcessor::SessionChangeProcessor( 59 DataTypeErrorHandler* error_handler, 60 SessionModelAssociator* session_model_associator) 61 : ChangeProcessor(error_handler), 62 weak_ptr_factory_(this), 63 session_model_associator_(session_model_associator), 64 profile_(NULL), 65 setup_for_test_(false) { 66 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 67 DCHECK(error_handler); 68 DCHECK(session_model_associator_); 69 } 70 71 SessionChangeProcessor::SessionChangeProcessor( 72 DataTypeErrorHandler* error_handler, 73 SessionModelAssociator* session_model_associator, 74 bool setup_for_test) 75 : ChangeProcessor(error_handler), 76 weak_ptr_factory_(this), 77 session_model_associator_(session_model_associator), 78 profile_(NULL), 79 setup_for_test_(setup_for_test) { 80 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 81 DCHECK(error_handler); 82 DCHECK(session_model_associator_); 83 } 84 85 SessionChangeProcessor::~SessionChangeProcessor() { 86 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 87 } 88 89 void SessionChangeProcessor::Observe( 90 int type, 91 const content::NotificationSource& source, 92 const content::NotificationDetails& details) { 93 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 94 DCHECK(profile_); 95 96 // Track which windows and/or tabs are modified. 97 std::vector<SyncedTabDelegate*> modified_tabs; 98 switch (type) { 99 case chrome::NOTIFICATION_FAVICON_CHANGED: { 100 content::Details<FaviconChangedDetails> favicon_details(details); 101 session_model_associator_->FaviconsUpdated(favicon_details->urls); 102 // Note: we favicon notifications don't affect tab contents, so we return 103 // here instead of continuing on to reassociate tabs/windows. 104 return; 105 } 106 107 case chrome::NOTIFICATION_BROWSER_OPENED: { 108 Browser* browser = content::Source<Browser>(source).ptr(); 109 if (!browser || browser->profile() != profile_) { 110 return; 111 } 112 DVLOG(1) << "Received BROWSER_OPENED for profile " << profile_; 113 break; 114 } 115 116 case chrome::NOTIFICATION_TAB_PARENTED: { 117 WebContents* web_contents = content::Source<WebContents>(source).ptr(); 118 SyncedTabDelegate* tab = 119 SyncedTabDelegate::ImplFromWebContents(web_contents); 120 if (!tab || tab->profile() != profile_) { 121 return; 122 } 123 modified_tabs.push_back(tab); 124 DVLOG(1) << "Received TAB_PARENTED for profile " << profile_; 125 break; 126 } 127 128 case content::NOTIFICATION_LOAD_COMPLETED_MAIN_FRAME: { 129 WebContents* web_contents = content::Source<WebContents>(source).ptr(); 130 SyncedTabDelegate* tab = 131 SyncedTabDelegate::ImplFromWebContents(web_contents); 132 if (!tab || tab->profile() != profile_) { 133 return; 134 } 135 modified_tabs.push_back(tab); 136 DVLOG(1) << "Received LOAD_COMPLETED_MAIN_FRAME for profile " << profile_; 137 break; 138 } 139 140 case content::NOTIFICATION_WEB_CONTENTS_DESTROYED: { 141 WebContents* web_contents = content::Source<WebContents>(source).ptr(); 142 SyncedTabDelegate* tab = 143 SyncedTabDelegate::ImplFromWebContents(web_contents); 144 if (!tab || tab->profile() != profile_) 145 return; 146 modified_tabs.push_back(tab); 147 DVLOG(1) << "Received NOTIFICATION_WEB_CONTENTS_DESTROYED for profile " 148 << profile_; 149 break; 150 } 151 152 case content::NOTIFICATION_NAV_LIST_PRUNED: { 153 SyncedTabDelegate* tab = ExtractSyncedTabDelegate(source); 154 if (!tab || tab->profile() != profile_) { 155 return; 156 } 157 modified_tabs.push_back(tab); 158 DVLOG(1) << "Received NAV_LIST_PRUNED for profile " << profile_; 159 break; 160 } 161 162 case content::NOTIFICATION_NAV_ENTRY_CHANGED: { 163 SyncedTabDelegate* tab = ExtractSyncedTabDelegate(source); 164 if (!tab || tab->profile() != profile_) { 165 return; 166 } 167 modified_tabs.push_back(tab); 168 DVLOG(1) << "Received NAV_ENTRY_CHANGED for profile " << profile_; 169 break; 170 } 171 172 case content::NOTIFICATION_NAV_ENTRY_COMMITTED: { 173 SyncedTabDelegate* tab = ExtractSyncedTabDelegate(source); 174 if (!tab || tab->profile() != profile_) { 175 return; 176 } 177 modified_tabs.push_back(tab); 178 DVLOG(1) << "Received NAV_ENTRY_COMMITTED for profile " << profile_; 179 break; 180 } 181 182 case chrome::NOTIFICATION_TAB_CONTENTS_APPLICATION_EXTENSION_CHANGED: { 183 extensions::TabHelper* extension_tab_helper = 184 content::Source<extensions::TabHelper>(source).ptr(); 185 if (extension_tab_helper->web_contents()->GetBrowserContext() != 186 profile_) { 187 return; 188 } 189 if (extension_tab_helper->extension_app()) { 190 SyncedTabDelegate* tab = SyncedTabDelegate::ImplFromWebContents( 191 extension_tab_helper->web_contents()); 192 modified_tabs.push_back(tab); 193 } 194 DVLOG(1) << "Received TAB_CONTENTS_APPLICATION_EXTENSION_CHANGED " 195 << "for profile " << profile_; 196 break; 197 } 198 199 default: 200 LOG(ERROR) << "Received unexpected notification of type " 201 << type; 202 break; 203 } 204 205 ProcessModifiedTabs(modified_tabs); 206 } 207 208 void SessionChangeProcessor::ApplyChangesFromSyncModel( 209 const syncer::BaseTransaction* trans, 210 int64 model_version, 211 const syncer::ImmutableChangeRecordList& changes) { 212 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 213 214 syncer::ReadNode root(trans); 215 if (root.InitByTagLookup(syncer::ModelTypeToRootTag(syncer::SESSIONS)) != 216 syncer::BaseNode::INIT_OK) { 217 error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE, 218 "Sessions root node lookup failed."); 219 return; 220 } 221 222 std::string local_tag = session_model_associator_->GetCurrentMachineTag(); 223 for (syncer::ChangeRecordList::const_iterator it = 224 changes.Get().begin(); it != changes.Get().end(); ++it) { 225 const syncer::ChangeRecord& change = *it; 226 syncer::ChangeRecord::Action action(change.action); 227 if (syncer::ChangeRecord::ACTION_DELETE == action) { 228 // Deletions are all or nothing (since we only ever delete entire 229 // sessions). Therefore we don't care if it's a tab node or meta node, 230 // and just ensure we've disassociated. 231 DCHECK_EQ(syncer::GetModelTypeFromSpecifics(it->specifics), 232 syncer::SESSIONS); 233 const sync_pb::SessionSpecifics& specifics = it->specifics.session(); 234 if (specifics.session_tag() == local_tag) { 235 // Another client has attempted to delete our local data (possibly by 236 // error or their/our clock is inaccurate). Just ignore the deletion 237 // for now to avoid any possible ping-pong delete/reassociate sequence. 238 LOG(WARNING) << "Local session data deleted. Ignoring until next local " 239 << "navigation event."; 240 } else { 241 if (specifics.has_header()) { 242 // Disassociate only when header node is deleted. For tab node 243 // deletions, the header node will be updated and foreign tab will 244 // get deleted. 245 session_model_associator_->DisassociateForeignSession( 246 specifics.session_tag()); 247 } 248 } 249 continue; 250 } 251 252 // Handle an update or add. 253 syncer::ReadNode sync_node(trans); 254 if (sync_node.InitByIdLookup(change.id) != syncer::BaseNode::INIT_OK) { 255 error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE, 256 "Session node lookup failed."); 257 return; 258 } 259 260 // Check that the changed node is a child of the session folder. 261 DCHECK(root.GetId() == sync_node.GetParentId()); 262 DCHECK(syncer::SESSIONS == sync_node.GetModelType()); 263 264 const sync_pb::SessionSpecifics& specifics( 265 sync_node.GetSessionSpecifics()); 266 if (specifics.session_tag() == local_tag && 267 !setup_for_test_) { 268 // We should only ever receive a change to our own machine's session info 269 // if encryption was turned on. In that case, the data is still the same, 270 // so we can ignore. 271 LOG(WARNING) << "Dropping modification to local session."; 272 return; 273 } 274 const base::Time& mtime = sync_node.GetModificationTime(); 275 // The model associator handles foreign session updates and adds the same. 276 session_model_associator_->AssociateForeignSpecifics(specifics, mtime); 277 } 278 279 // Notify foreign session handlers that there are new sessions. 280 content::NotificationService::current()->Notify( 281 chrome::NOTIFICATION_FOREIGN_SESSION_UPDATED, 282 content::Source<Profile>(profile_), 283 content::NotificationService::NoDetails()); 284 } 285 286 void SessionChangeProcessor::StartImpl(Profile* profile) { 287 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 288 DCHECK(profile); 289 DCHECK(profile_ == NULL); 290 profile_ = profile; 291 StartObserving(); 292 } 293 294 void SessionChangeProcessor::OnNavigationBlocked(WebContents* web_contents) { 295 SyncedTabDelegate* tab = 296 SyncedTabDelegate::ImplFromWebContents(web_contents); 297 if (!tab) 298 return; 299 300 DCHECK(tab->profile() == profile_); 301 302 std::vector<SyncedTabDelegate*> modified_tabs; 303 modified_tabs.push_back(tab); 304 ProcessModifiedTabs(modified_tabs); 305 } 306 307 void SessionChangeProcessor::ProcessModifiedTabs( 308 const std::vector<SyncedTabDelegate*>& modified_tabs) { 309 // Check if this tab should trigger a session sync refresh. By virtue of 310 // it being a modified tab, we know the tab is active (so we won't do 311 // refreshes just because the refresh page is open in a background tab). 312 if (!modified_tabs.empty()) { 313 SyncedTabDelegate* tab = modified_tabs.front(); 314 const content::NavigationEntry* entry = tab->GetActiveEntry(); 315 if (!tab->IsBeingDestroyed() && 316 entry && 317 entry->GetVirtualURL().is_valid() && 318 entry->GetVirtualURL().spec() == kNTPOpenTabSyncURL) { 319 DVLOG(1) << "Triggering sync refresh for sessions datatype."; 320 const syncer::ModelTypeSet types(syncer::SESSIONS); 321 content::NotificationService::current()->Notify( 322 chrome::NOTIFICATION_SYNC_REFRESH_LOCAL, 323 content::Source<Profile>(profile_), 324 content::Details<const syncer::ModelTypeSet>(&types)); 325 } 326 } 327 328 // Associate tabs first so the synced session tracker is aware of them. 329 // Note that if we fail to associate, it means something has gone wrong, 330 // such as our local session being deleted, so we disassociate and associate 331 // again. 332 bool reassociation_needed = !modified_tabs.empty() && 333 !session_model_associator_->AssociateTabs(modified_tabs, NULL); 334 335 // Note, we always associate windows because it's possible a tab became 336 // "interesting" by going to a valid URL, in which case it needs to be added 337 // to the window's tab information. 338 if (!reassociation_needed) { 339 reassociation_needed = 340 !session_model_associator_->AssociateWindows(false, NULL); 341 } 342 343 if (reassociation_needed) { 344 LOG(WARNING) << "Reassociation of local models triggered."; 345 syncer::SyncError error; 346 error = session_model_associator_->DisassociateModels(); 347 error = session_model_associator_->AssociateModels(NULL, NULL); 348 if (error.IsSet()) { 349 error_handler()->OnSingleDatatypeUnrecoverableError( 350 error.location(), 351 error.message()); 352 } 353 } 354 } 355 356 void SessionChangeProcessor::StartObserving() { 357 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 358 if (!profile_) 359 return; 360 notification_registrar_.Add(this, chrome::NOTIFICATION_TAB_PARENTED, 361 content::NotificationService::AllSources()); 362 notification_registrar_.Add(this, 363 content::NOTIFICATION_WEB_CONTENTS_DESTROYED, 364 content::NotificationService::AllSources()); 365 notification_registrar_.Add(this, content::NOTIFICATION_NAV_LIST_PRUNED, 366 content::NotificationService::AllSources()); 367 notification_registrar_.Add(this, content::NOTIFICATION_NAV_ENTRY_CHANGED, 368 content::NotificationService::AllSources()); 369 notification_registrar_.Add(this, content::NOTIFICATION_NAV_ENTRY_COMMITTED, 370 content::NotificationService::AllSources()); 371 notification_registrar_.Add(this, chrome::NOTIFICATION_BROWSER_OPENED, 372 content::NotificationService::AllBrowserContextsAndSources()); 373 notification_registrar_.Add(this, 374 chrome::NOTIFICATION_TAB_CONTENTS_APPLICATION_EXTENSION_CHANGED, 375 content::NotificationService::AllSources()); 376 notification_registrar_.Add(this, 377 content::NOTIFICATION_LOAD_COMPLETED_MAIN_FRAME, 378 content::NotificationService::AllBrowserContextsAndSources()); 379 notification_registrar_.Add(this, chrome::NOTIFICATION_FAVICON_CHANGED, 380 content::Source<Profile>(profile_)); 381 #if defined(ENABLE_MANAGED_USERS) 382 if (profile_->IsManaged()) { 383 ManagedUserService* managed_user_service = 384 ManagedUserServiceFactory::GetForProfile(profile_); 385 managed_user_service->AddNavigationBlockedCallback( 386 base::Bind(&SessionChangeProcessor::OnNavigationBlocked, 387 weak_ptr_factory_.GetWeakPtr())); 388 } 389 #endif 390 } 391 392 } // namespace browser_sync 393