// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/sync/glue/session_change_processor.h" #include #include #include "base/logging.h" #include "chrome/browser/chrome_notification_types.h" #include "chrome/browser/extensions/tab_helper.h" #include "chrome/browser/favicon/favicon_changed_details.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/sync/glue/session_model_associator.h" #include "chrome/browser/sync/glue/synced_tab_delegate.h" #include "chrome/browser/sync/profile_sync_service.h" #include "content/public/browser/navigation_controller.h" #include "content/public/browser/navigation_entry.h" #include "content/public/browser/notification_details.h" #include "content/public/browser/notification_service.h" #include "content/public/browser/notification_source.h" #include "content/public/browser/web_contents.h" #include "sync/api/sync_error.h" #include "sync/internal_api/public/base/model_type.h" #include "sync/internal_api/public/change_record.h" #include "sync/internal_api/public/read_node.h" #include "sync/protocol/session_specifics.pb.h" #if defined(ENABLE_MANAGED_USERS) #include "chrome/browser/managed_mode/managed_user_service.h" #include "chrome/browser/managed_mode/managed_user_service_factory.h" #endif using content::BrowserThread; using content::NavigationController; using content::WebContents; namespace browser_sync { namespace { // The URL at which the set of synced tabs is displayed. We treat it differently // from all other URL's as accessing it triggers a sync refresh of Sessions. static const char kNTPOpenTabSyncURL[] = "chrome://newtab/#open_tabs"; // Extract the source SyncedTabDelegate from a NotificationSource originating // from a NavigationController, if it exists. Returns |NULL| otherwise. SyncedTabDelegate* ExtractSyncedTabDelegate( const content::NotificationSource& source) { return SyncedTabDelegate::ImplFromWebContents( content::Source(source).ptr()->GetWebContents()); } } // namespace SessionChangeProcessor::SessionChangeProcessor( DataTypeErrorHandler* error_handler, SessionModelAssociator* session_model_associator) : ChangeProcessor(error_handler), weak_ptr_factory_(this), session_model_associator_(session_model_associator), profile_(NULL), setup_for_test_(false) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(error_handler); DCHECK(session_model_associator_); } SessionChangeProcessor::SessionChangeProcessor( DataTypeErrorHandler* error_handler, SessionModelAssociator* session_model_associator, bool setup_for_test) : ChangeProcessor(error_handler), weak_ptr_factory_(this), session_model_associator_(session_model_associator), profile_(NULL), setup_for_test_(setup_for_test) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(error_handler); DCHECK(session_model_associator_); } SessionChangeProcessor::~SessionChangeProcessor() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); } void SessionChangeProcessor::Observe( int type, const content::NotificationSource& source, const content::NotificationDetails& details) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(profile_); // Track which windows and/or tabs are modified. std::vector modified_tabs; switch (type) { case chrome::NOTIFICATION_FAVICON_CHANGED: { content::Details favicon_details(details); session_model_associator_->FaviconsUpdated(favicon_details->urls); // Note: we favicon notifications don't affect tab contents, so we return // here instead of continuing on to reassociate tabs/windows. return; } case chrome::NOTIFICATION_BROWSER_OPENED: { Browser* browser = content::Source(source).ptr(); if (!browser || browser->profile() != profile_) { return; } DVLOG(1) << "Received BROWSER_OPENED for profile " << profile_; break; } case chrome::NOTIFICATION_TAB_PARENTED: { WebContents* web_contents = content::Source(source).ptr(); SyncedTabDelegate* tab = SyncedTabDelegate::ImplFromWebContents(web_contents); if (!tab || tab->profile() != profile_) { return; } modified_tabs.push_back(tab); DVLOG(1) << "Received TAB_PARENTED for profile " << profile_; break; } case content::NOTIFICATION_LOAD_COMPLETED_MAIN_FRAME: { WebContents* web_contents = content::Source(source).ptr(); SyncedTabDelegate* tab = SyncedTabDelegate::ImplFromWebContents(web_contents); if (!tab || tab->profile() != profile_) { return; } modified_tabs.push_back(tab); DVLOG(1) << "Received LOAD_COMPLETED_MAIN_FRAME for profile " << profile_; break; } case content::NOTIFICATION_WEB_CONTENTS_DESTROYED: { WebContents* web_contents = content::Source(source).ptr(); SyncedTabDelegate* tab = SyncedTabDelegate::ImplFromWebContents(web_contents); if (!tab || tab->profile() != profile_) return; modified_tabs.push_back(tab); DVLOG(1) << "Received NOTIFICATION_WEB_CONTENTS_DESTROYED for profile " << profile_; break; } case content::NOTIFICATION_NAV_LIST_PRUNED: { SyncedTabDelegate* tab = ExtractSyncedTabDelegate(source); if (!tab || tab->profile() != profile_) { return; } modified_tabs.push_back(tab); DVLOG(1) << "Received NAV_LIST_PRUNED for profile " << profile_; break; } case content::NOTIFICATION_NAV_ENTRY_CHANGED: { SyncedTabDelegate* tab = ExtractSyncedTabDelegate(source); if (!tab || tab->profile() != profile_) { return; } modified_tabs.push_back(tab); DVLOG(1) << "Received NAV_ENTRY_CHANGED for profile " << profile_; break; } case content::NOTIFICATION_NAV_ENTRY_COMMITTED: { SyncedTabDelegate* tab = ExtractSyncedTabDelegate(source); if (!tab || tab->profile() != profile_) { return; } modified_tabs.push_back(tab); DVLOG(1) << "Received NAV_ENTRY_COMMITTED for profile " << profile_; break; } case chrome::NOTIFICATION_TAB_CONTENTS_APPLICATION_EXTENSION_CHANGED: { extensions::TabHelper* extension_tab_helper = content::Source(source).ptr(); if (extension_tab_helper->web_contents()->GetBrowserContext() != profile_) { return; } if (extension_tab_helper->extension_app()) { SyncedTabDelegate* tab = SyncedTabDelegate::ImplFromWebContents( extension_tab_helper->web_contents()); modified_tabs.push_back(tab); } DVLOG(1) << "Received TAB_CONTENTS_APPLICATION_EXTENSION_CHANGED " << "for profile " << profile_; break; } default: LOG(ERROR) << "Received unexpected notification of type " << type; break; } ProcessModifiedTabs(modified_tabs); } void SessionChangeProcessor::ApplyChangesFromSyncModel( const syncer::BaseTransaction* trans, int64 model_version, const syncer::ImmutableChangeRecordList& changes) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); syncer::ReadNode root(trans); if (root.InitByTagLookup(syncer::ModelTypeToRootTag(syncer::SESSIONS)) != syncer::BaseNode::INIT_OK) { error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE, "Sessions root node lookup failed."); return; } std::string local_tag = session_model_associator_->GetCurrentMachineTag(); for (syncer::ChangeRecordList::const_iterator it = changes.Get().begin(); it != changes.Get().end(); ++it) { const syncer::ChangeRecord& change = *it; syncer::ChangeRecord::Action action(change.action); if (syncer::ChangeRecord::ACTION_DELETE == action) { // Deletions are all or nothing (since we only ever delete entire // sessions). Therefore we don't care if it's a tab node or meta node, // and just ensure we've disassociated. DCHECK_EQ(syncer::GetModelTypeFromSpecifics(it->specifics), syncer::SESSIONS); const sync_pb::SessionSpecifics& specifics = it->specifics.session(); if (specifics.session_tag() == local_tag) { // Another client has attempted to delete our local data (possibly by // error or their/our clock is inaccurate). Just ignore the deletion // for now to avoid any possible ping-pong delete/reassociate sequence. LOG(WARNING) << "Local session data deleted. Ignoring until next local " << "navigation event."; } else { if (specifics.has_header()) { // Disassociate only when header node is deleted. For tab node // deletions, the header node will be updated and foreign tab will // get deleted. session_model_associator_->DisassociateForeignSession( specifics.session_tag()); } } continue; } // Handle an update or add. syncer::ReadNode sync_node(trans); if (sync_node.InitByIdLookup(change.id) != syncer::BaseNode::INIT_OK) { error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE, "Session node lookup failed."); return; } // Check that the changed node is a child of the session folder. DCHECK(root.GetId() == sync_node.GetParentId()); DCHECK(syncer::SESSIONS == sync_node.GetModelType()); const sync_pb::SessionSpecifics& specifics( sync_node.GetSessionSpecifics()); if (specifics.session_tag() == local_tag && !setup_for_test_) { // We should only ever receive a change to our own machine's session info // if encryption was turned on. In that case, the data is still the same, // so we can ignore. LOG(WARNING) << "Dropping modification to local session."; return; } const base::Time& mtime = sync_node.GetModificationTime(); // The model associator handles foreign session updates and adds the same. session_model_associator_->AssociateForeignSpecifics(specifics, mtime); } // Notify foreign session handlers that there are new sessions. content::NotificationService::current()->Notify( chrome::NOTIFICATION_FOREIGN_SESSION_UPDATED, content::Source(profile_), content::NotificationService::NoDetails()); } void SessionChangeProcessor::StartImpl(Profile* profile) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(profile); DCHECK(profile_ == NULL); profile_ = profile; StartObserving(); } void SessionChangeProcessor::OnNavigationBlocked(WebContents* web_contents) { SyncedTabDelegate* tab = SyncedTabDelegate::ImplFromWebContents(web_contents); if (!tab) return; DCHECK(tab->profile() == profile_); std::vector modified_tabs; modified_tabs.push_back(tab); ProcessModifiedTabs(modified_tabs); } void SessionChangeProcessor::ProcessModifiedTabs( const std::vector& modified_tabs) { // Check if this tab should trigger a session sync refresh. By virtue of // it being a modified tab, we know the tab is active (so we won't do // refreshes just because the refresh page is open in a background tab). if (!modified_tabs.empty()) { SyncedTabDelegate* tab = modified_tabs.front(); const content::NavigationEntry* entry = tab->GetActiveEntry(); if (!tab->IsBeingDestroyed() && entry && entry->GetVirtualURL().is_valid() && entry->GetVirtualURL().spec() == kNTPOpenTabSyncURL) { DVLOG(1) << "Triggering sync refresh for sessions datatype."; const syncer::ModelTypeSet types(syncer::SESSIONS); content::NotificationService::current()->Notify( chrome::NOTIFICATION_SYNC_REFRESH_LOCAL, content::Source(profile_), content::Details(&types)); } } // Associate tabs first so the synced session tracker is aware of them. // Note that if we fail to associate, it means something has gone wrong, // such as our local session being deleted, so we disassociate and associate // again. bool reassociation_needed = !modified_tabs.empty() && !session_model_associator_->AssociateTabs(modified_tabs, NULL); // Note, we always associate windows because it's possible a tab became // "interesting" by going to a valid URL, in which case it needs to be added // to the window's tab information. if (!reassociation_needed) { reassociation_needed = !session_model_associator_->AssociateWindows(false, NULL); } if (reassociation_needed) { LOG(WARNING) << "Reassociation of local models triggered."; syncer::SyncError error; error = session_model_associator_->DisassociateModels(); error = session_model_associator_->AssociateModels(NULL, NULL); if (error.IsSet()) { error_handler()->OnSingleDatatypeUnrecoverableError( error.location(), error.message()); } } } void SessionChangeProcessor::StartObserving() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); if (!profile_) return; notification_registrar_.Add(this, chrome::NOTIFICATION_TAB_PARENTED, content::NotificationService::AllSources()); notification_registrar_.Add(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED, content::NotificationService::AllSources()); notification_registrar_.Add(this, content::NOTIFICATION_NAV_LIST_PRUNED, content::NotificationService::AllSources()); notification_registrar_.Add(this, content::NOTIFICATION_NAV_ENTRY_CHANGED, content::NotificationService::AllSources()); notification_registrar_.Add(this, content::NOTIFICATION_NAV_ENTRY_COMMITTED, content::NotificationService::AllSources()); notification_registrar_.Add(this, chrome::NOTIFICATION_BROWSER_OPENED, content::NotificationService::AllBrowserContextsAndSources()); notification_registrar_.Add(this, chrome::NOTIFICATION_TAB_CONTENTS_APPLICATION_EXTENSION_CHANGED, content::NotificationService::AllSources()); notification_registrar_.Add(this, content::NOTIFICATION_LOAD_COMPLETED_MAIN_FRAME, content::NotificationService::AllBrowserContextsAndSources()); notification_registrar_.Add(this, chrome::NOTIFICATION_FAVICON_CHANGED, content::Source(profile_)); #if defined(ENABLE_MANAGED_USERS) if (profile_->IsManaged()) { ManagedUserService* managed_user_service = ManagedUserServiceFactory::GetForProfile(profile_); managed_user_service->AddNavigationBlockedCallback( base::Bind(&SessionChangeProcessor::OnNavigationBlocked, weak_ptr_factory_.GetWeakPtr())); } #endif } } // namespace browser_sync