blob: efc51ee24360a6068a02358458d2ad62d9f7b703 [file] [log] [blame]
// 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 <string>
#include <vector>
#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<NavigationController>(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<SyncedTabDelegate*> modified_tabs;
switch (type) {
case chrome::NOTIFICATION_FAVICON_CHANGED: {
content::Details<FaviconChangedDetails> 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<Browser>(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<WebContents>(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<WebContents>(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<WebContents>(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<extensions::TabHelper>(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>(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<SyncedTabDelegate*> modified_tabs;
modified_tabs.push_back(tab);
ProcessModifiedTabs(modified_tabs);
}
void SessionChangeProcessor::ProcessModifiedTabs(
const std::vector<SyncedTabDelegate*>& 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>(profile_),
content::Details<const syncer::ModelTypeSet>(&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>(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