root/chrome/browser/sync/glue/session_change_processor.cc

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. ExtractSyncedTabDelegate
  2. weak_ptr_factory_
  3. weak_ptr_factory_
  4. Observe
  5. ApplyChangesFromSyncModel
  6. StartImpl
  7. OnNavigationBlocked
  8. ProcessModifiedTabs
  9. StartObserving

// 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),
      session_model_associator_(session_model_associator),
      profile_(NULL),
      setup_for_test_(false),
      weak_ptr_factory_(this) {
  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),
      session_model_associator_(session_model_associator),
      profile_(NULL),
      setup_for_test_(setup_for_test),
      weak_ptr_factory_(this) {
  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

/* [<][>][^][v][top][bottom][index][help] */