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.
5 #include "chrome/browser/sync/glue/session_change_processor.h"
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/change_record.h"
27 #include "sync/internal_api/public/read_node.h"
28 #include "sync/protocol/session_specifics.pb.h"
30 #if defined(ENABLE_MANAGED_USERS)
31 #include "chrome/browser/managed_mode/managed_user_service.h"
32 #include "chrome/browser/managed_mode/managed_user_service_factory.h"
35 using content::BrowserThread
;
36 using content::NavigationController
;
37 using content::WebContents
;
39 namespace browser_sync
{
43 // The URL at which the set of synced tabs is displayed. We treat it differently
44 // from all other URL's as accessing it triggers a sync refresh of Sessions.
45 static const char kNTPOpenTabSyncURL
[] = "chrome://newtab/#open_tabs";
47 // Extract the source SyncedTabDelegate from a NotificationSource originating
48 // from a NavigationController, if it exists. Returns |NULL| otherwise.
49 SyncedTabDelegate
* ExtractSyncedTabDelegate(
50 const content::NotificationSource
& source
) {
51 return SyncedTabDelegate::ImplFromWebContents(
52 content::Source
<NavigationController
>(source
).ptr()->GetWebContents());
57 SessionChangeProcessor::SessionChangeProcessor(
58 DataTypeErrorHandler
* error_handler
,
59 SessionModelAssociator
* session_model_associator
)
60 : ChangeProcessor(error_handler
),
61 session_model_associator_(session_model_associator
),
63 setup_for_test_(false),
64 weak_ptr_factory_(this) {
65 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
66 DCHECK(error_handler
);
67 DCHECK(session_model_associator_
);
70 SessionChangeProcessor::SessionChangeProcessor(
71 DataTypeErrorHandler
* error_handler
,
72 SessionModelAssociator
* session_model_associator
,
74 : ChangeProcessor(error_handler
),
75 session_model_associator_(session_model_associator
),
77 setup_for_test_(setup_for_test
),
78 weak_ptr_factory_(this) {
79 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
80 DCHECK(error_handler
);
81 DCHECK(session_model_associator_
);
84 SessionChangeProcessor::~SessionChangeProcessor() {
85 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
88 void SessionChangeProcessor::Observe(
90 const content::NotificationSource
& source
,
91 const content::NotificationDetails
& details
) {
92 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
95 // Track which windows and/or tabs are modified.
96 std::vector
<SyncedTabDelegate
*> modified_tabs
;
98 case chrome::NOTIFICATION_FAVICON_CHANGED
: {
99 content::Details
<FaviconChangedDetails
> favicon_details(details
);
100 session_model_associator_
->FaviconsUpdated(favicon_details
->urls
);
101 // Note: we favicon notifications don't affect tab contents, so we return
102 // here instead of continuing on to reassociate tabs/windows.
106 case chrome::NOTIFICATION_BROWSER_OPENED
: {
107 Browser
* browser
= content::Source
<Browser
>(source
).ptr();
108 if (!browser
|| browser
->profile() != profile_
) {
111 DVLOG(1) << "Received BROWSER_OPENED for profile " << profile_
;
115 case chrome::NOTIFICATION_TAB_PARENTED
: {
116 WebContents
* web_contents
= content::Source
<WebContents
>(source
).ptr();
117 SyncedTabDelegate
* tab
=
118 SyncedTabDelegate::ImplFromWebContents(web_contents
);
119 if (!tab
|| tab
->profile() != profile_
) {
122 modified_tabs
.push_back(tab
);
123 DVLOG(1) << "Received TAB_PARENTED for profile " << profile_
;
127 case content::NOTIFICATION_LOAD_COMPLETED_MAIN_FRAME
: {
128 WebContents
* web_contents
= content::Source
<WebContents
>(source
).ptr();
129 SyncedTabDelegate
* tab
=
130 SyncedTabDelegate::ImplFromWebContents(web_contents
);
131 if (!tab
|| tab
->profile() != profile_
) {
134 modified_tabs
.push_back(tab
);
135 DVLOG(1) << "Received LOAD_COMPLETED_MAIN_FRAME for profile " << profile_
;
139 case content::NOTIFICATION_WEB_CONTENTS_DESTROYED
: {
140 WebContents
* web_contents
= content::Source
<WebContents
>(source
).ptr();
141 SyncedTabDelegate
* tab
=
142 SyncedTabDelegate::ImplFromWebContents(web_contents
);
143 if (!tab
|| tab
->profile() != profile_
)
145 modified_tabs
.push_back(tab
);
146 DVLOG(1) << "Received NOTIFICATION_WEB_CONTENTS_DESTROYED for profile "
151 case content::NOTIFICATION_NAV_LIST_PRUNED
: {
152 SyncedTabDelegate
* tab
= ExtractSyncedTabDelegate(source
);
153 if (!tab
|| tab
->profile() != profile_
) {
156 modified_tabs
.push_back(tab
);
157 DVLOG(1) << "Received NAV_LIST_PRUNED for profile " << profile_
;
161 case content::NOTIFICATION_NAV_ENTRY_CHANGED
: {
162 SyncedTabDelegate
* tab
= ExtractSyncedTabDelegate(source
);
163 if (!tab
|| tab
->profile() != profile_
) {
166 modified_tabs
.push_back(tab
);
167 DVLOG(1) << "Received NAV_ENTRY_CHANGED for profile " << profile_
;
171 case content::NOTIFICATION_NAV_ENTRY_COMMITTED
: {
172 SyncedTabDelegate
* tab
= ExtractSyncedTabDelegate(source
);
173 if (!tab
|| tab
->profile() != profile_
) {
176 modified_tabs
.push_back(tab
);
177 DVLOG(1) << "Received NAV_ENTRY_COMMITTED for profile " << profile_
;
181 case chrome::NOTIFICATION_TAB_CONTENTS_APPLICATION_EXTENSION_CHANGED
: {
182 extensions::TabHelper
* extension_tab_helper
=
183 content::Source
<extensions::TabHelper
>(source
).ptr();
184 if (extension_tab_helper
->web_contents()->GetBrowserContext() !=
188 if (extension_tab_helper
->extension_app()) {
189 SyncedTabDelegate
* tab
= SyncedTabDelegate::ImplFromWebContents(
190 extension_tab_helper
->web_contents());
191 modified_tabs
.push_back(tab
);
193 DVLOG(1) << "Received TAB_CONTENTS_APPLICATION_EXTENSION_CHANGED "
194 << "for profile " << profile_
;
199 LOG(ERROR
) << "Received unexpected notification of type "
204 ProcessModifiedTabs(modified_tabs
);
207 void SessionChangeProcessor::ApplyChangesFromSyncModel(
208 const syncer::BaseTransaction
* trans
,
210 const syncer::ImmutableChangeRecordList
& changes
) {
211 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
213 syncer::ReadNode
root(trans
);
214 if (root
.InitByTagLookup(syncer::ModelTypeToRootTag(syncer::SESSIONS
)) !=
215 syncer::BaseNode::INIT_OK
) {
216 error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE
,
217 "Sessions root node lookup failed.");
221 std::string local_tag
= session_model_associator_
->GetCurrentMachineTag();
222 for (syncer::ChangeRecordList::const_iterator it
=
223 changes
.Get().begin(); it
!= changes
.Get().end(); ++it
) {
224 const syncer::ChangeRecord
& change
= *it
;
225 syncer::ChangeRecord::Action
action(change
.action
);
226 if (syncer::ChangeRecord::ACTION_DELETE
== action
) {
227 // Deletions are all or nothing (since we only ever delete entire
228 // sessions). Therefore we don't care if it's a tab node or meta node,
229 // and just ensure we've disassociated.
230 DCHECK_EQ(syncer::GetModelTypeFromSpecifics(it
->specifics
),
232 const sync_pb::SessionSpecifics
& specifics
= it
->specifics
.session();
233 if (specifics
.session_tag() == local_tag
) {
234 // Another client has attempted to delete our local data (possibly by
235 // error or their/our clock is inaccurate). Just ignore the deletion
236 // for now to avoid any possible ping-pong delete/reassociate sequence.
237 LOG(WARNING
) << "Local session data deleted. Ignoring until next local "
238 << "navigation event.";
240 if (specifics
.has_header()) {
241 // Disassociate only when header node is deleted. For tab node
242 // deletions, the header node will be updated and foreign tab will
244 session_model_associator_
->DisassociateForeignSession(
245 specifics
.session_tag());
251 // Handle an update or add.
252 syncer::ReadNode
sync_node(trans
);
253 if (sync_node
.InitByIdLookup(change
.id
) != syncer::BaseNode::INIT_OK
) {
254 error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE
,
255 "Session node lookup failed.");
259 // Check that the changed node is a child of the session folder.
260 DCHECK(root
.GetId() == sync_node
.GetParentId());
261 DCHECK(syncer::SESSIONS
== sync_node
.GetModelType());
263 const sync_pb::SessionSpecifics
& specifics(
264 sync_node
.GetSessionSpecifics());
265 if (specifics
.session_tag() == local_tag
&&
267 // We should only ever receive a change to our own machine's session info
268 // if encryption was turned on. In that case, the data is still the same,
270 LOG(WARNING
) << "Dropping modification to local session.";
273 const base::Time
& mtime
= sync_node
.GetModificationTime();
274 // The model associator handles foreign session updates and adds the same.
275 session_model_associator_
->AssociateForeignSpecifics(specifics
, mtime
);
278 // Notify foreign session handlers that there are new sessions.
279 content::NotificationService::current()->Notify(
280 chrome::NOTIFICATION_FOREIGN_SESSION_UPDATED
,
281 content::Source
<Profile
>(profile_
),
282 content::NotificationService::NoDetails());
285 void SessionChangeProcessor::StartImpl(Profile
* profile
) {
286 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
288 DCHECK(profile_
== NULL
);
293 void SessionChangeProcessor::OnNavigationBlocked(WebContents
* web_contents
) {
294 SyncedTabDelegate
* tab
=
295 SyncedTabDelegate::ImplFromWebContents(web_contents
);
299 DCHECK(tab
->profile() == profile_
);
301 std::vector
<SyncedTabDelegate
*> modified_tabs
;
302 modified_tabs
.push_back(tab
);
303 ProcessModifiedTabs(modified_tabs
);
306 void SessionChangeProcessor::ProcessModifiedTabs(
307 const std::vector
<SyncedTabDelegate
*>& modified_tabs
) {
308 // Check if this tab should trigger a session sync refresh. By virtue of
309 // it being a modified tab, we know the tab is active (so we won't do
310 // refreshes just because the refresh page is open in a background tab).
311 if (!modified_tabs
.empty()) {
312 SyncedTabDelegate
* tab
= modified_tabs
.front();
313 const content::NavigationEntry
* entry
= tab
->GetActiveEntry();
314 if (!tab
->IsBeingDestroyed() &&
316 entry
->GetVirtualURL().is_valid() &&
317 entry
->GetVirtualURL().spec() == kNTPOpenTabSyncURL
) {
318 DVLOG(1) << "Triggering sync refresh for sessions datatype.";
319 const syncer::ModelTypeSet
types(syncer::SESSIONS
);
320 content::NotificationService::current()->Notify(
321 chrome::NOTIFICATION_SYNC_REFRESH_LOCAL
,
322 content::Source
<Profile
>(profile_
),
323 content::Details
<const syncer::ModelTypeSet
>(&types
));
327 // Associate tabs first so the synced session tracker is aware of them.
328 // Note that if we fail to associate, it means something has gone wrong,
329 // such as our local session being deleted, so we disassociate and associate
331 bool reassociation_needed
= !modified_tabs
.empty() &&
332 !session_model_associator_
->AssociateTabs(modified_tabs
, NULL
);
334 // Note, we always associate windows because it's possible a tab became
335 // "interesting" by going to a valid URL, in which case it needs to be added
336 // to the window's tab information.
337 if (!reassociation_needed
) {
338 reassociation_needed
=
339 !session_model_associator_
->AssociateWindows(false, NULL
);
342 if (reassociation_needed
) {
343 LOG(WARNING
) << "Reassociation of local models triggered.";
344 syncer::SyncError error
;
345 error
= session_model_associator_
->DisassociateModels();
346 error
= session_model_associator_
->AssociateModels(NULL
, NULL
);
348 error_handler()->OnSingleDatatypeUnrecoverableError(
355 void SessionChangeProcessor::StartObserving() {
356 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
359 notification_registrar_
.Add(this, chrome::NOTIFICATION_TAB_PARENTED
,
360 content::NotificationService::AllSources());
361 notification_registrar_
.Add(this,
362 content::NOTIFICATION_WEB_CONTENTS_DESTROYED
,
363 content::NotificationService::AllSources());
364 notification_registrar_
.Add(this, content::NOTIFICATION_NAV_LIST_PRUNED
,
365 content::NotificationService::AllSources());
366 notification_registrar_
.Add(this, content::NOTIFICATION_NAV_ENTRY_CHANGED
,
367 content::NotificationService::AllSources());
368 notification_registrar_
.Add(this, content::NOTIFICATION_NAV_ENTRY_COMMITTED
,
369 content::NotificationService::AllSources());
370 notification_registrar_
.Add(this, chrome::NOTIFICATION_BROWSER_OPENED
,
371 content::NotificationService::AllBrowserContextsAndSources());
372 notification_registrar_
.Add(this,
373 chrome::NOTIFICATION_TAB_CONTENTS_APPLICATION_EXTENSION_CHANGED
,
374 content::NotificationService::AllSources());
375 notification_registrar_
.Add(this,
376 content::NOTIFICATION_LOAD_COMPLETED_MAIN_FRAME
,
377 content::NotificationService::AllBrowserContextsAndSources());
378 notification_registrar_
.Add(this, chrome::NOTIFICATION_FAVICON_CHANGED
,
379 content::Source
<Profile
>(profile_
));
380 #if defined(ENABLE_MANAGED_USERS)
381 if (profile_
->IsManaged()) {
382 ManagedUserService
* managed_user_service
=
383 ManagedUserServiceFactory::GetForProfile(profile_
);
384 managed_user_service
->AddNavigationBlockedCallback(
385 base::Bind(&SessionChangeProcessor::OnNavigationBlocked
,
386 weak_ptr_factory_
.GetWeakPtr()));
391 } // namespace browser_sync