Home | History | Annotate | Download | only in glue
      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