summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--WATCHLISTS5
-rw-r--r--chrome/browser/browser.cc2
-rw-r--r--chrome/browser/browser_resources.grd5
-rw-r--r--chrome/browser/dom_ui/chrome_url_data_manager.cc12
-rw-r--r--chrome/browser/dom_ui/dom_ui_factory.cc2
-rw-r--r--chrome/browser/dom_ui/new_tab_page_sync_handler.cc200
-rw-r--r--chrome/browser/dom_ui/new_tab_page_sync_handler.h61
-rw-r--r--chrome/browser/dom_ui/new_tab_ui.cc6
-rw-r--r--chrome/browser/gtk/options/options_window_gtk.cc6
-rw-r--r--chrome/browser/profile.h4
-rw-r--r--chrome/browser/sync/auth_error_state.h25
-rw-r--r--chrome/browser/sync/engine/syncapi.h709
-rw-r--r--chrome/browser/sync/glue/bookmark_model_worker.cc114
-rw-r--r--chrome/browser/sync/glue/bookmark_model_worker.h134
-rw-r--r--chrome/browser/sync/glue/bookmark_model_worker_unittest.cc224
-rw-r--r--chrome/browser/sync/glue/http_bridge.cc252
-rw-r--r--chrome/browser/sync/glue/http_bridge.h173
-rw-r--r--chrome/browser/sync/glue/http_bridge_unittest.cc167
-rw-r--r--chrome/browser/sync/glue/model_associator.cc504
-rw-r--r--chrome/browser/sync/glue/model_associator.h141
-rw-r--r--chrome/browser/sync/glue/sync_backend_host.cc308
-rw-r--r--chrome/browser/sync/glue/sync_backend_host.h274
-rw-r--r--chrome/browser/sync/personalization.cc339
-rw-r--r--chrome/browser/sync/personalization.h109
-rw-r--r--chrome/browser/sync/personalization_strings.h69
-rw-r--r--chrome/browser/sync/profile_sync_service.cc887
-rw-r--r--chrome/browser/sync/profile_sync_service.h368
-rw-r--r--chrome/browser/sync/profile_sync_service_unittest.cc1273
-rw-r--r--chrome/browser/sync/resources/about_sync.html165
-rw-r--r--chrome/browser/sync/resources/gaia_login.html331
-rw-r--r--chrome/browser/sync/resources/merge_and_sync.html66
-rw-r--r--chrome/browser/sync/resources/new_tab_personalization.html117
-rw-r--r--chrome/browser/sync/resources/setup_flow.html19
-rw-r--r--chrome/browser/sync/sync_status_ui_helper.cc101
-rw-r--r--chrome/browser/sync/sync_status_ui_helper.h34
-rw-r--r--chrome/browser/views/options/options_window_view.cc8
-rw-r--r--chrome/browser/views/options/user_data_page_view.cc186
-rw-r--r--chrome/browser/views/options/user_data_page_view.h88
-rw-r--r--chrome/browser/views/sync/sync_setup_flow.cc240
-rw-r--r--chrome/browser/views/sync/sync_setup_flow.h176
-rw-r--r--chrome/browser/views/sync/sync_setup_wizard.cc57
-rw-r--r--chrome/browser/views/sync/sync_setup_wizard.h50
-rw-r--r--chrome/browser/views/sync/sync_setup_wizard_unittest.cc329
-rw-r--r--chrome/browser/views/toolbar_view.cc2
44 files changed, 8321 insertions, 21 deletions
diff --git a/WATCHLISTS b/WATCHLISTS
index a348fef..4992568 100644
--- a/WATCHLISTS
+++ b/WATCHLISTS
@@ -75,6 +75,9 @@
'linux-sandboxing': {
'filepath': 'sandbox/linux/.*',
},
+ 'sync': {
+ 'filepath': 'chrome/browser/sync/.*',
+ },
},
'WATCHLISTS': {
@@ -102,5 +105,7 @@
'scherkus@chromium.org'],
'ipc': ['jam@chromium.org'],
'linux-sandboxing': ['agl@chromium.org'],
+ 'sync': ['tim@chromium.org', 'nick@chromium.org',
+ 'idana@chromium.org'],
},
}
diff --git a/chrome/browser/browser.cc b/chrome/browser/browser.cc
index dd2f07b..712ef35 100644
--- a/chrome/browser/browser.cc
+++ b/chrome/browser/browser.cc
@@ -51,7 +51,7 @@
#include "chrome/common/pref_service.h"
#include "chrome/common/url_constants.h"
#ifdef CHROME_PERSONALIZATION
-#include "chrome/personalization/personalization.h"
+#include "chrome/browser/sync/personalization.h"
#endif
#include "grit/chromium_strings.h"
#include "grit/generated_resources.h"
diff --git a/chrome/browser/browser_resources.grd b/chrome/browser/browser_resources.grd
index 218cd07..71476ba 100644
--- a/chrome/browser/browser_resources.grd
+++ b/chrome/browser/browser_resources.grd
@@ -40,6 +40,11 @@ without changes to the corresponding grd file. -->
<include name="IDR_LINUX_SPLASH_HTML_CHROMIUM" file="resources\linux-splash.html" flattenhtml="true" type="BINDATA" />
<include name="IDR_LINUX_SPLASH_HTML_CHROME" file="resources\linux-splash-chrome.html" flattenhtml="true" type="BINDATA" />
</if>
+ <include name="IDR_ABOUT_SYNC_HTML" file="sync\resources\about_sync.html" flattenhtml="true" type="BINDATA" />
+ <include name="IDR_GAIA_LOGIN_HTML" file="sync\resources\gaia_login.html" flattenhtml="true" type="BINDATA" />
+ <include name="IDR_SYNC_SETUP_FLOW_HTML" file="sync\resources\setup_flow.html" flattenhtml="true" type="BINDATA" />
+ <include name="IDR_MERGE_AND_SYNC_HTML" file="sync\resources\merge_and_sync.html" flattenhtml="true" type="BINDATA" />
+ <include name="IDR_NEW_TAB_P13N_HTML" file="sync\resources\new_tab_personalization.html" flattenhtml="true" type="BINDATA" />
</includes>
</release>
</grit>
diff --git a/chrome/browser/dom_ui/chrome_url_data_manager.cc b/chrome/browser/dom_ui/chrome_url_data_manager.cc
index 36fde4d..dfc08ce 100644
--- a/chrome/browser/dom_ui/chrome_url_data_manager.cc
+++ b/chrome/browser/dom_ui/chrome_url_data_manager.cc
@@ -12,6 +12,12 @@
#include "base/thread.h"
#include "base/values.h"
#include "chrome/browser/browser_process.h"
+#ifdef CHROME_PERSONALIZATION
+// TODO(timsteele): Remove all CHROME_PERSONALIZATION code in this file.
+// It is only temporarily needed to configure some personalization data sources
+// that will go away soon.
+#include "chrome/browser/sync/personalization.h"
+#endif
#include "chrome/common/chrome_paths.h"
#include "chrome/common/ref_counted_util.h"
#include "chrome/common/url_constants.h"
@@ -21,12 +27,6 @@
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_file_job.h"
#include "net/url_request/url_request_job.h"
-#ifdef CHROME_PERSONALIZATION
-// TODO(timsteele): Remove all CHROME_PERSONALIZATION code in this file.
-// It is only temporarily needed to configure some personalization data sources
-// that will go away soon.
-#include "chrome/personalization/personalization.h"
-#endif
#include "grit/locale_settings.h"
diff --git a/chrome/browser/dom_ui/dom_ui_factory.cc b/chrome/browser/dom_ui/dom_ui_factory.cc
index d58dc48..af214c6 100644
--- a/chrome/browser/dom_ui/dom_ui_factory.cc
+++ b/chrome/browser/dom_ui/dom_ui_factory.cc
@@ -17,7 +17,7 @@
#include "chrome/browser/tab_contents/tab_contents.h"
#include "chrome/common/url_constants.h"
#ifdef CHROME_PERSONALIZATION
-#include "chrome/personalization/personalization.h"
+#include "chrome/browser/sync/personalization.h"
#endif
#include "googleurl/src/gurl.h"
diff --git a/chrome/browser/dom_ui/new_tab_page_sync_handler.cc b/chrome/browser/dom_ui/new_tab_page_sync_handler.cc
new file mode 100644
index 0000000..3a30e03a
--- /dev/null
+++ b/chrome/browser/dom_ui/new_tab_page_sync_handler.cc
@@ -0,0 +1,200 @@
+// Copyright (c) 2006-2008 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#include "chrome/browser/dom_ui/new_tab_page_sync_handler.h"
+
+#include "base/json_writer.h"
+#include "base/string_util.h"
+#include "base/values.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/renderer_host/render_view_host.h"
+#include "chrome/browser/sync/personalization.h"
+#include "chrome/browser/sync/personalization_strings.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/common/pref_service.h"
+#include "grit/browser_resources.h"
+#include "net/base/cookie_monster.h"
+#include "net/url_request/url_request_context.h"
+
+// XPath expression for finding our p13n iframe.
+static const wchar_t* kP13nIframeXpath = L"//iframe[@id='p13n']";
+
+namespace Personalization {
+
+// A simple pair of fields that identify a set of Google cookies, used to
+// filter from a larger set.
+struct GoogleCookieFilter {
+ // The generalized, fully qualified URL of pages where
+ // cookies with id |cookie_id| are obtained / accessed.
+ const char* url;
+ // The id of the cookie this filter is selecting,
+ // with name/value delimiter (i.e '=').
+ const char* cookie_id;
+};
+
+// Filters to select Google GAIA cookies.
+static const GoogleCookieFilter kGAIACookieFilters[] = {
+ { "http://.google.com/", "SID=" }, // Gmail.
+ // Add filters here for other interesting cookies that should result in
+ // showing the promotions (e.g ASIDAS for dasher accounts).
+};
+
+bool IsGoogleGAIACookieInstalled() {
+ for (size_t i = 0; i < arraysize(kGAIACookieFilters); ++i) {
+ URLRequestContext* context = Profile::GetDefaultRequestContext();
+ net::CookieStore* store = context->cookie_store();
+ GURL url(kGAIACookieFilters[i].url);
+ net::CookieOptions options;
+ options.set_include_httponly(); // The SID cookie might be httponly.
+ std::string cookies = store->GetCookiesWithOptions(url, options);
+ std::vector<std::string> cookie_list;
+ SplitString(cookies, ';', &cookie_list);
+ for (std::vector<std::string>::iterator current = cookie_list.begin();
+ current != cookie_list.end();
+ ++current) {
+ size_t position =
+ current->find(kGAIACookieFilters[i].cookie_id);
+ if (0 == position)
+ return true;
+ }
+ }
+ return false;
+}
+
+} // namespace Personalization
+
+NewTabPageSyncHandler::NewTabPageSyncHandler() : sync_service_(NULL),
+ waiting_for_initial_page_load_(true) {
+}
+
+NewTabPageSyncHandler::~NewTabPageSyncHandler() {
+ sync_service_->RemoveObserver(this);
+}
+
+DOMMessageHandler* NewTabPageSyncHandler::Attach(DOMUI* dom_ui) {
+ Profile* p = dom_ui->GetProfile();
+ sync_service_ = p->GetProfilePersonalization()->sync_service();
+ DCHECK(sync_service_);
+ sync_service_->AddObserver(this);
+ return DOMMessageHandler::Attach(dom_ui);
+}
+
+void NewTabPageSyncHandler::RegisterMessages() {
+ dom_ui_->RegisterMessageCallback("GetSyncMessage",
+ NewCallback(this, &NewTabPageSyncHandler::HandleGetSyncMessage));
+ dom_ui_->RegisterMessageCallback("SyncLinkClicked",
+ NewCallback(this, &NewTabPageSyncHandler::HandleSyncLinkClicked));
+ dom_ui_->RegisterMessageCallback("ResizeP13N",
+ NewCallback(this, &NewTabPageSyncHandler::HandleResizeP13N));
+}
+
+void NewTabPageSyncHandler::HandleResizeP13N(const Value* value) {
+ // We just want to call back in to the new tab page on behalf of our
+ // same-origin-policy crippled iframe, to tell it to resize the container.
+ dom_ui_->CallJavascriptFunction(L"resizeP13N", *value);
+}
+
+void NewTabPageSyncHandler::BuildAndSendSyncStatus() {
+ DCHECK(!waiting_for_initial_page_load_);
+
+ if (!sync_service_->IsSyncEnabledByUser() &&
+ !sync_service_->SetupInProgress()) {
+ // Clear the page status, without showing the promotion or sync ui.
+ // TODO(timsteele): This is fine, but if the page is refreshed or another
+ // NTP is opened, we could end up showing the promo again. Not sure this is
+ // desired if the user already signed up once and disabled.
+ FundamentalValue value(0);
+ dom_ui_->CallJavascriptFunction(L"resizeP13N", value);
+ return;
+ }
+
+ // There are currently three supported "sync statuses" for the NTP, from
+ // the users perspective:
+ // "Synced to foo@gmail.com", when we are successfully authenticated and
+ // connected to a sync server.
+ // "Sync error", when we can't authenticate or establish a connection with
+ // the sync server (appropriate information appended to
+ // message).
+ // "Authenticating", when credentials are in flight.
+ SyncStatusUIHelper::MessageType type(SyncStatusUIHelper::PRE_SYNCED);
+ std::wstring status_msg;
+ std::wstring link_text;
+ type = SyncStatusUIHelper::GetLabels(sync_service_, &status_msg, &link_text);
+ SendSyncMessageToPage(type, WideToUTF8(status_msg), WideToUTF8(link_text));
+}
+
+void NewTabPageSyncHandler::HandleGetSyncMessage(const Value* value) {
+ waiting_for_initial_page_load_ = false;
+
+ if (!sync_service_->IsSyncEnabledByUser() &&
+ !sync_service_->SetupInProgress()) {
+ if (Personalization::IsGoogleGAIACookieInstalled()) {
+ // Sync has not been enabled, and the user has logged in to GAIA.
+ SendSyncMessageToPage(SyncStatusUIHelper::PRE_SYNCED, kSyncPromotionMsg,
+ kStartNowLinkText);
+ }
+ return;
+ }
+
+ BuildAndSendSyncStatus();
+}
+
+void NewTabPageSyncHandler::HandleSyncLinkClicked(const Value* value) {
+ DCHECK(!waiting_for_initial_page_load_);
+ if (sync_service_->IsSyncEnabledByUser()) {
+ // User clicked 'Login again' link to re-authenticate.
+ sync_service_->ShowLoginDialog();
+ } else {
+ // User clicked "Start now" link to begin syncing.
+ sync_service_->EnableForUser();
+ }
+}
+
+void NewTabPageSyncHandler::OnStateChanged() {
+ // Don't do anything if the page has not yet loaded.
+ if (waiting_for_initial_page_load_)
+ return;
+ BuildAndSendSyncStatus();
+}
+
+void NewTabPageSyncHandler::SendSyncMessageToPage(
+ SyncStatusUIHelper::MessageType type, std::string msg,
+ const std::string& linktext) {
+ DictionaryValue value;
+ std::string title, msgtype;
+ switch (type) {
+ case SyncStatusUIHelper::PRE_SYNCED:
+ title = kSyncSectionTitle;
+ msgtype = "presynced";
+ break;
+ case SyncStatusUIHelper::SYNCED:
+ msgtype = "synced";
+ msg = msg.substr(0, msg.find(WideToUTF8(kLastSyncedLabel)));
+ break;
+ case SyncStatusUIHelper::SYNC_ERROR:
+ title = kSyncErrorSectionTitle;
+ msgtype = "error";
+ break;
+ }
+
+ value.SetString(L"title", title);
+ value.SetString(L"msg", msg);
+ value.SetString(L"msgtype", msgtype);
+ if (!linktext.empty())
+ value.SetString(L"linktext", linktext);
+ else
+ value.SetBoolean(L"linktext", false);
+
+ std::string json;
+ JSONWriter::Write(&value, false, &json);
+ std::wstring javascript = std::wstring(L"renderSyncMessage") +
+ L"(" + UTF8ToWide(json) + L");";
+ RenderViewHost* rvh = dom_ui_->tab_contents()->render_view_host();
+ rvh->ExecuteJavascriptInWebFrame(kP13nIframeXpath, javascript);
+}
+
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/dom_ui/new_tab_page_sync_handler.h b/chrome/browser/dom_ui/new_tab_page_sync_handler.h
new file mode 100644
index 0000000..2f50a2a
--- /dev/null
+++ b/chrome/browser/dom_ui/new_tab_page_sync_handler.h
@@ -0,0 +1,61 @@
+// Copyright (c) 2006-2008 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#ifndef CHROME_BROWSER_DOM_UI_NEW_TAB_PAGE_SYNC_HANDLER_H_
+#define CHROME_BROWSER_DOM_UI_NEW_TAB_PAGE_SYNC_HANDLER_H_
+
+#include <string>
+
+#include "chrome/browser/dom_ui/dom_ui.h"
+#include "chrome/browser/sync/profile_sync_service.h"
+#include "chrome/browser/sync/sync_status_ui_helper.h"
+
+class Value;
+
+// Sends sync-state changes to the New Tab Page for UI updating and forwards
+// link clicks on the page to the sync service.
+class NewTabPageSyncHandler : public DOMMessageHandler,
+ public ProfileSyncServiceObserver {
+ public:
+ NewTabPageSyncHandler();
+ virtual ~NewTabPageSyncHandler();
+
+ // DOMMessageHandler implementation.
+ virtual DOMMessageHandler* Attach(DOMUI* dom_ui);
+ virtual void RegisterMessages();
+
+ // Callback for "GetSyncMessage".
+ void HandleGetSyncMessage(const Value* value);
+ // Callback for "SyncLinkClicked".
+ void HandleSyncLinkClicked(const Value* value);
+ // Callback for "ResizeP13N"
+ void HandleResizeP13N(const Value* value);
+
+ // ProfileSyncServiceObserver
+ virtual void OnStateChanged();
+
+ private:
+ // Helper to invoke |renderSyncMessage| JS function on the new tab page.
+ void SendSyncMessageToPage(SyncStatusUIHelper::MessageType type,
+ std::string msg, const std::string& linktext);
+
+ // Helper to query the sync service and figure out what to send to
+ // the page, and send it via SendSyncMessageToPage.
+ // NOTE: precondition: sync must be enabled.
+ void BuildAndSendSyncStatus();
+
+ // Cached pointer to ProfileSyncService.
+ ProfileSyncService* sync_service_;
+
+ // Used to make sure we don't register ourselves twice if the user refreshes
+ // the new tab page.
+ bool waiting_for_initial_page_load_;
+
+ DISALLOW_COPY_AND_ASSIGN(NewTabPageSyncHandler);
+};
+
+#endif // CHROME_BROWSER_DOM_UI_NEW_TAB_PAGE_SYNC_HANDLER_H_
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/dom_ui/new_tab_ui.cc b/chrome/browser/dom_ui/new_tab_ui.cc
index f8f4fda..d5463a5 100644
--- a/chrome/browser/dom_ui/new_tab_ui.cc
+++ b/chrome/browser/dom_ui/new_tab_ui.cc
@@ -36,6 +36,9 @@
#include "chrome/browser/tab_contents/tab_contents.h"
#include "chrome/browser/search_engines/template_url.h"
#include "chrome/browser/sessions/tab_restore_service.h"
+#ifdef CHROME_PERSONALIZATION
+#include "chrome/browser/sync/personalization.h"
+#endif
#include "chrome/browser/user_data_manager.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/jstemplate_builder.h"
@@ -43,9 +46,6 @@
#include "chrome/common/pref_names.h"
#include "chrome/common/pref_service.h"
#include "chrome/common/url_constants.h"
-#ifdef CHROME_PERSONALIZATION
-#include "chrome/personalization/personalization.h"
-#endif
#include "grit/browser_resources.h"
#include "grit/chromium_strings.h"
#include "grit/generated_resources.h"
diff --git a/chrome/browser/gtk/options/options_window_gtk.cc b/chrome/browser/gtk/options/options_window_gtk.cc
index 86e188a..8ee1591 100644
--- a/chrome/browser/gtk/options/options_window_gtk.cc
+++ b/chrome/browser/gtk/options/options_window_gtk.cc
@@ -13,13 +13,13 @@
#include "chrome/browser/gtk/options/content_page_gtk.h"
#include "chrome/browser/gtk/options/general_page_gtk.h"
#include "chrome/browser/profile.h"
+#ifdef CHROME_PERSONALIZATION
+#include "chrome/browser/sync/personalization.h"
+#endif
#include "chrome/common/gtk_util.h"
#include "chrome/common/pref_member.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/pref_service.h"
-#ifdef CHROME_PERSONALIZATION
-#include "chrome/personalization/personalization.h"
-#endif
#include "grit/chromium_strings.h"
#include "grit/generated_resources.h"
diff --git a/chrome/browser/profile.h b/chrome/browser/profile.h
index 656af37..2eacb43 100644
--- a/chrome/browser/profile.h
+++ b/chrome/browser/profile.h
@@ -15,10 +15,10 @@
#include "base/file_path.h"
#include "base/scoped_ptr.h"
#include "base/timer.h"
-#include "chrome/browser/web_resource/web_resource_service.h"
#ifdef CHROME_PERSONALIZATION
-#include "chrome/personalization/personalization.h"
+#include "chrome/browser/sync/personalization.h"
#endif
+#include "chrome/browser/web_resource/web_resource_service.h"
#include "chrome/common/notification_registrar.h"
namespace net {
diff --git a/chrome/browser/sync/auth_error_state.h b/chrome/browser/sync/auth_error_state.h
new file mode 100644
index 0000000..8f29f46
--- /dev/null
+++ b/chrome/browser/sync/auth_error_state.h
@@ -0,0 +1,25 @@
+// Copyright (c) 2006-2008 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.
+
+#ifndef CHROME_BROWSER_SYNC_AUTH_ERROR_STATE_H_
+#define CHROME_BROWSER_SYNC_AUTH_ERROR_STATE_H_
+
+#include <string>
+#include "base/string_util.h"
+
+enum AuthErrorState {
+ AUTH_ERROR_NONE = 0,
+ // The credentials supplied to GAIA were either invalid, or the locally
+ // cached credentials have expired. If this happens, the sync system
+ // will continue as if offline until authentication is reattempted.
+ AUTH_ERROR_INVALID_GAIA_CREDENTIALS,
+ // The GAIA user is not authorized to use the sync service.
+ AUTH_ERROR_USER_NOT_SIGNED_UP,
+ // Could not connect to server to verify credentials. This could be in
+ // response to either failure to connect to GAIA or failure to connect to
+ // the service needing GAIA tokens during authentication.
+ AUTH_ERROR_CONNECTION_FAILED,
+};
+
+#endif // CHROME_BROWSER_SYNC_AUTH_ERROR_STATE_H_
diff --git a/chrome/browser/sync/engine/syncapi.h b/chrome/browser/sync/engine/syncapi.h
new file mode 100644
index 0000000..da50cc2
--- /dev/null
+++ b/chrome/browser/sync/engine/syncapi.h
@@ -0,0 +1,709 @@
+// Copyright (c) 2009 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.
+
+// This file defines the "sync API", an interface to the syncer
+// backend that exposes (1) the core functionality of maintaining a consistent
+// local snapshot of a hierarchical object set; (2) a means to transactionally
+// access and modify those objects; (3) a means to control client/server
+// synchronization tasks, namely: pushing local object modifications to a
+// server, pulling nonlocal object modifications from a server to this client,
+// and resolving conflicts that may arise between the two; and (4) an
+// abstraction of some external functionality that is to be provided by the
+// host environment.
+//
+// This interface is used as the entry point into the syncer backend
+// when the backend is compiled as a library and embedded in another
+// application. A goal for this interface layer is to depend on very few
+// external types, so that an application can use the sync backend
+// without introducing a dependency on specific types. A non-goal is to
+// have binary compatibility across versions or compilers; this allows the
+// interface to use C++ classes. An application wishing to use the sync API
+// should ideally compile the syncer backend and this API as part of the
+// application's own build, to avoid e.g. mismatches in calling convention,
+// structure padding, or name mangling that could arise if there were a
+// compiler mismatch.
+//
+// The schema of the objects in the sync domain is based on the model, which
+// is essentially a hierarchy of items and folders similar to a filesystem,
+// but with a few important differences. The sync API contains fields
+// such as URL to easily allow the embedding application to store web
+// browser bookmarks. Also, the sync API allows duplicate titles in a parent.
+// Consequently, it does not support looking up an object by title
+// and parent, since such a lookup is not uniquely determined. Lastly,
+// unlike a filesystem model, objects in the Sync API model have a strict
+// ordering within a parent; the position is manipulable by callers, and
+// children of a node can be enumerated in the order of their position.
+
+#ifndef CHROME_BROWSER_SYNC_ENGINE_SYNCAPI_H_
+#define CHROME_BROWSER_SYNC_ENGINE_SYNCAPI_H_
+
+#include "base/basictypes.h"
+
+#if (defined(OS_WIN) || defined(OS_WINDOWS))
+typedef wchar_t sync_char16;
+#else
+typedef uint16 sync_char16;
+#endif
+
+// The MSVC compiler for Windows requires that any classes exported by, or
+// imported from, a dynamic library be decorated with the following fanciness.
+#if (defined(OS_WIN) || defined(OS_WINDOWS))
+#if COMPILING_SYNCAPI_LIBRARY
+#define SYNC_EXPORT __declspec(dllexport)
+#else
+#define SYNC_EXPORT __declspec(dllimport)
+#endif
+#else
+#define SYNC_EXPORT
+#endif // OS_WIN || OS_WINDOWS
+
+// Forward declarations of internal class types so that sync API objects
+// may have opaque pointers to these types.
+namespace syncable {
+class BaseTransaction;
+class DirectoryManager;
+class Entry;
+class MutableEntry;
+class ReadTransaction;
+class ScopedDirLookup;
+class WriteTransaction;
+}
+
+namespace sync_api {
+
+// Forward declarations of classes to be defined later in this file.
+class BaseTransaction;
+class HttpPostProviderFactory;
+class ModelSafeWorkerInterface;
+class SyncManager;
+class WriteTransaction;
+struct UserShare;
+
+// A valid BaseNode will never have an ID of zero.
+static const int64 kInvalidId = 0;
+
+// BaseNode wraps syncable::Entry, and corresponds to a single object's state.
+// This, like syncable::Entry, is intended for use on the stack. A valid
+// transaction is necessary to create a BaseNode or any of its children.
+// Unlike syncable::Entry, a sync API BaseNode is identified primarily by its
+// int64 metahandle, which we call an ID here.
+class SYNC_EXPORT BaseNode {
+ public:
+ // All subclasses of BaseNode must provide a way to initialize themselves by
+ // doing an ID lookup. Returns false on failure. An invalid or deleted
+ // ID will result in failure.
+ virtual bool InitByIdLookup(int64 id) = 0;
+
+ // Each object is identified by a 64-bit id (internally, the syncable
+ // metahandle). These ids are strictly local handles. They will persist
+ // on this client, but the same object on a different client may have a
+ // different ID value.
+ int64 GetId() const;
+
+ // Nodes are hierarchically arranged into a single-rooted tree.
+ // InitByRootLookup on ReadNode allows access to the root. GetParentId is
+ // how you find a node's parent.
+ int64 GetParentId() const;
+
+ // Nodes are either folders or not. This corresponds to the IS_DIR property
+ // of syncable::Entry.
+ bool GetIsFolder() const;
+
+ // Returns the title of the object as a C string. The memory is owned by
+ // BaseNode and becomes invalid if GetTitle() is called a second time on this
+ // node, or when the node is destroyed. A caller should convert this
+ // immediately into e.g. a std::string. Uniqueness of the title is not
+ // enforced on siblings -- it is not an error for two children to share
+ // a title.
+ const sync_char16* GetTitle() const;
+
+ // Returns the URL of a bookmark object as a C string. The memory is owned
+ // by BaseNode and becomes invalid if GetURL() is called a second time on
+ // this node, or when the node is destroyed. A caller should convert this
+ // immediately into e.g. a std::string.
+ const sync_char16* GetURL() const;
+
+ // Return a pointer to the byte data of the favicon image for this node.
+ // Will return NULL if there is no favicon data associated with this node.
+ // The length of the array is returned to the caller via |size_in_bytes|.
+ // Favicons are expected to be PNG images, and though no verification is
+ // done on the syncapi client of this, the server may reject favicon updates
+ // that are invalid for whatever reason.
+ const unsigned char* GetFaviconBytes(size_t* size_in_bytes);
+
+ // Returns the local external ID associated with the node.
+ int64 GetExternalId() const;
+
+ // Return the ID of the node immediately before this in the sibling order.
+ // For the first node in the ordering, return 0.
+ int64 GetPredecessorId() const;
+
+ // Return the ID of the node immediately after this in the sibling order.
+ // For the last node in the ordering, return 0.
+ int64 GetSuccessorId() const;
+
+ // Return the ID of the first child of this node. If this node has no
+ // children, return 0.
+ int64 GetFirstChildId() const;
+
+ // Get an array containing the IDs of this node's children. The memory is
+ // owned by BaseNode and becomes invalid if GetChildIds() is called a second
+ // time on this node, or when the node is destroyed. Return the array size
+ // in the child_count parameter.
+ const int64* GetChildIds(size_t* child_count) const;
+
+ // These virtual accessors provide access to data members of derived classes.
+ virtual const syncable::Entry* GetEntry() const = 0;
+ virtual const BaseTransaction* GetTransaction() const = 0;
+
+ protected:
+ BaseNode();
+ virtual ~BaseNode();
+
+ private:
+ struct BaseNodeInternal;
+
+ // Node is meant for stack use only.
+ void* operator new(size_t size);
+
+ // Provides storage for member functions that return pointers to class
+ // memory, e.g. C strings returned by GetTitle().
+ BaseNodeInternal* data_;
+
+ DISALLOW_COPY_AND_ASSIGN(BaseNode);
+};
+
+// WriteNode extends BaseNode to add mutation, and wraps
+// syncable::MutableEntry. A WriteTransaction is needed to create a WriteNode.
+class SYNC_EXPORT WriteNode : public BaseNode {
+ public:
+ // Create a WriteNode using the given transaction.
+ explicit WriteNode(WriteTransaction* transaction);
+ virtual ~WriteNode();
+
+ // A client must use one (and only one) of the following Init variants to
+ // populate the node.
+
+ // BaseNode implementation.
+ virtual bool InitByIdLookup(int64 id);
+
+ // Create a new node with the specified parent and predecessor. Use a NULL
+ // |predecessor| to indicate that this is to be the first child.
+ // |predecessor| must be a child of |new_parent| or NULL. Returns false on
+ // failure.
+ bool InitByCreation(const BaseNode& parent, const BaseNode* predecessor);
+
+ // These Set() functions correspond to the Get() functions of BaseNode.
+ void SetIsFolder(bool folder);
+ void SetTitle(const sync_char16* title);
+ void SetURL(const sync_char16* url);
+ void SetFaviconBytes(const unsigned char* bytes, size_t size_in_bytes);
+ // External ID is a client-only field, so setting it doesn't cause the item to
+ // be synced again.
+ void SetExternalId(int64 external_id);
+
+ // Remove this node and its children.
+ void Remove();
+
+ // Set a new parent and position. Position is specified by |predecessor|; if
+ // it is NULL, the node is moved to the first position. |predecessor| must
+ // be a child of |new_parent| or NULL. Returns false on failure..
+ bool SetPosition(const BaseNode& new_parent, const BaseNode* predecessor);
+
+ // Implementation of BaseNode's abstract virtual accessors.
+ virtual const syncable::Entry* GetEntry() const;
+
+ virtual const BaseTransaction* GetTransaction() const;
+
+ private:
+ void* operator new(size_t size); // Node is meant for stack use only.
+
+ // Helper to set the previous node.
+ void PutPredecessor(const BaseNode* predecessor);
+
+ // Sets IS_UNSYNCED and SYNCING to ensure this entry is considered in an
+ // upcoming commit pass.
+ void MarkForSyncing();
+
+ // The underlying syncable object which this class wraps.
+ syncable::MutableEntry* entry_;
+
+ // The sync API transaction that is the parent of this node.
+ WriteTransaction* transaction_;
+
+ DISALLOW_COPY_AND_ASSIGN(WriteNode);
+};
+
+// ReadNode wraps a syncable::Entry to provide the functionality of a
+// read-only BaseNode.
+class SYNC_EXPORT ReadNode : public BaseNode {
+ public:
+ // Create an unpopulated ReadNode on the given transaction. Call some flavor
+ // of Init to populate the ReadNode with a database entry.
+ explicit ReadNode(const BaseTransaction* transaction);
+ virtual ~ReadNode();
+
+ // A client must use one (and only one) of the following Init variants to
+ // populate the node.
+
+ // BaseNode implementation.
+ virtual bool InitByIdLookup(int64 id);
+
+ // There is always a root node, so this can't fail. The root node is
+ // never mutable, so root lookup is only possible on a ReadNode.
+ void InitByRootLookup();
+
+ // Each server-created permanent node is tagged with a unique string.
+ // Look up the node with the particular tag. If it does not exist,
+ // return false. Since these nodes are special, lookup is only
+ // provided only through ReadNode.
+ bool InitByTagLookup(const sync_char16* tag);
+
+ // Implementation of BaseNode's abstract virtual accessors.
+ virtual const syncable::Entry* GetEntry() const;
+ virtual const BaseTransaction* GetTransaction() const;
+
+ private:
+ void* operator new(size_t size); // Node is meant for stack use only.
+
+ // The underlying syncable object which this class wraps.
+ syncable::Entry* entry_;
+
+ // The sync API transaction that is the parent of this node.
+ const BaseTransaction* transaction_;
+
+ DISALLOW_COPY_AND_ASSIGN(ReadNode);
+};
+
+// Sync API's BaseTransaction, ReadTransaction, and WriteTransaction allow for
+// batching of several read and/or write operations. The read and write
+// operations are performed by creating ReadNode and WriteNode instances using
+// the transaction. These transaction classes wrap identically named classes in
+// syncable, and are used in a similar way. Unlike syncable::BaseTransaction,
+// whose construction requires an explicit syncable::ScopedDirLookup, a sync
+// API BaseTransaction creates its own ScopedDirLookup implicitly.
+class SYNC_EXPORT BaseTransaction {
+ public:
+ // Provide access to the underlying syncable.h objects from BaseNode.
+ virtual syncable::BaseTransaction* GetWrappedTrans() const = 0;
+ const syncable::ScopedDirLookup& GetLookup() const { return *lookup_; }
+
+ protected:
+ // The ScopedDirLookup is created in the constructor and destroyed
+ // in the destructor. Creation of the ScopedDirLookup is not expected
+ // to fail.
+ explicit BaseTransaction(UserShare* share);
+ virtual ~BaseTransaction();
+
+ private:
+ // A syncable ScopedDirLookup, which is the parent of syncable transactions.
+ syncable::ScopedDirLookup* lookup_;
+
+ DISALLOW_COPY_AND_ASSIGN(BaseTransaction);
+};
+
+// Sync API's ReadTransaction is a read-only BaseTransaction. It wraps
+// a syncable::ReadTransaction.
+class SYNC_EXPORT ReadTransaction : public BaseTransaction {
+ public:
+ // Start a new read-only transaction on the specified repository.
+ explicit ReadTransaction(UserShare* share);
+ virtual ~ReadTransaction();
+
+ // BaseTransaction override.
+ virtual syncable::BaseTransaction* GetWrappedTrans() const;
+ private:
+ void* operator new(size_t size); // Transaction is meant for stack use only.
+
+ // The underlying syncable object which this class wraps.
+ syncable::ReadTransaction* transaction_;
+
+ DISALLOW_COPY_AND_ASSIGN(ReadTransaction);
+};
+
+// Sync API's WriteTransaction is a read/write BaseTransaction. It wraps
+// a syncable::WriteTransaction.
+class SYNC_EXPORT WriteTransaction : public BaseTransaction {
+ public:
+ // Start a new read/write transaction.
+ explicit WriteTransaction(UserShare* share);
+ virtual ~WriteTransaction();
+
+ // Provide access to the syncable.h transaction from the API WriteNode.
+ virtual syncable::BaseTransaction* GetWrappedTrans() const;
+ syncable::WriteTransaction* GetWrappedWriteTrans() { return transaction_; }
+
+ private:
+ void* operator new(size_t size); // Transaction is meant for stack use only.
+
+ // The underlying syncable object which this class wraps.
+ syncable::WriteTransaction* transaction_;
+
+ DISALLOW_COPY_AND_ASSIGN(WriteTransaction);
+};
+
+// SyncManager encapsulates syncable::DirectoryManager and serves as the parent
+// of all other objects in the sync API. SyncManager is thread-safe. If
+// multiple threads interact with the same local sync repository (i.e. the
+// same sqlite database), they should share a single SyncManager instance. The
+// caller should typically create one SyncManager for the lifetime of a user
+// session.
+class SYNC_EXPORT SyncManager {
+ public:
+ // SyncInternal contains the implementation of SyncManager, while abstracting
+ // internal types from clients of the interface.
+ class SyncInternal;
+
+ // ChangeRecord indicates a single item that changed as a result of a sync
+ // operation. This gives the sync id of the node that changed, and the type
+ // of change. To get the actual property values after an ADD or UPDATE, the
+ // client should get the node with InitByIdLookup(), using the provided id.
+ struct ChangeRecord {
+ enum Action {
+ ACTION_ADD,
+ ACTION_DELETE,
+ ACTION_UPDATE,
+ };
+ ChangeRecord() : id(kInvalidId), action(ACTION_ADD) {}
+ int64 id;
+ Action action;
+ };
+
+ // When the SyncManager is unable to initiate the syncing process due to a
+ // failure during authentication, AuthProblem describes the actual problem
+ // more precisely.
+ enum AuthProblem {
+ AUTH_PROBLEM_NONE = 0,
+ // The credentials supplied to GAIA were either invalid, or the locally
+ // cached credentials have expired. If this happens, the sync system
+ // will continue as if offline until authentication is reattempted.
+ AUTH_PROBLEM_INVALID_GAIA_CREDENTIALS,
+ // The GAIA user is not authorized to use the sync service.
+ AUTH_PROBLEM_USER_NOT_SIGNED_UP,
+ // Could not connect to server to verify credentials. This could be in
+ // response to either failure to connect to GAIA or failure to connect to
+ // the sync service during authentication.
+ AUTH_PROBLEM_CONNECTION_FAILED,
+ };
+
+ // Status encapsulates detailed state about the internals of the SyncManager.
+ struct Status {
+ // Summary is a distilled set of important information that the end-user may
+ // wish to be informed about (through UI, for example). Note that if a
+ // summary state requires user interaction (such as auth failures), more
+ // detailed information may be contained in additional status fields.
+ enum Summary {
+ // The internal instance is in an unrecognizable state. This should not
+ // happen.
+ INVALID = 0,
+ // Can't connect to server, but there are no pending changes in
+ // our local cache.
+ OFFLINE,
+ // Can't connect to server, and there are pending changes in our
+ // local cache.
+ OFFLINE_UNSYNCED,
+ // Connected and syncing.
+ SYNCING,
+ // Connected, no pending changes.
+ READY,
+ // User has chosen to pause syncing.
+ PAUSED,
+ // Internal sync error.
+ CONFLICT,
+ // Can't connect to server, and we haven't completed the initial
+ // sync yet. So there's nothing we can do but wait for the server.
+ OFFLINE_UNUSABLE,
+ };
+ Summary summary;
+
+ // Various server related information.
+ bool authenticated; // Successfully authenticated via GAIA.
+ bool server_up; // True if we have received at least one good
+ // reply from the server.
+ bool server_reachable; // True if we received any reply from the server.
+ bool server_broken; // True of the syncer is stopped because of server
+ // issues.
+
+ bool notifications_enabled; // True only if subscribed for notifications.
+ int notifications_received;
+ int notifications_sent;
+
+ // Various Syncer data.
+ int unsynced_count;
+ int conflicting_count;
+ bool syncing;
+ bool syncer_paused;
+ bool initial_sync_ended;
+ bool syncer_stuck;
+ int64 updates_available;
+ int64 updates_received;
+ bool disk_full;
+ bool invalid_store;
+ int max_consecutive_errors; // The max number of errors from any component.
+ };
+
+ // An interface the embedding application implements to receive notifications
+ // from the SyncManager. Register an observer via SyncManager::AddObserver.
+ // This observer is an event driven model as the events may be raised from
+ // different internal threads, and simply providing an "OnStatusChanged" type
+ // notification complicates things such as trying to determine "what changed",
+ // if different members of the Status object are modified from different
+ // threads. This way, the event is explicit, and it is safe for the Observer
+ // to dispatch to a native thread or synchronize accordingly.
+ class Observer {
+ public:
+ Observer() { }
+ virtual ~Observer() { }
+ // Notify the observer that changes have been applied to the sync model.
+ // This will be invoked on the same thread as on which ApplyChanges was
+ // called. |changes| is an array of size |change_count|, and contains the ID
+ // of each individual item that was changed. |changes| exists only
+ // for the duration of the call. Because the observer is passed a |trans|,
+ // the observer can assume a read lock on the database that will be released
+ // after the function returns.
+ //
+ // The SyncManager constructs |changes| in the following guaranteed order:
+ //
+ // 1. Deletions, from leaves up to parents.
+ // 2. Updates to existing items with synced parents & predecessors.
+ // 3. New items with synced parents & predecessors.
+ // 4. Items with parents & predecessors in |changes|.
+ // 5. Repeat #4 until all items are in |changes|.
+ //
+ // Thus, an implementation of OnChangesApplied should be able to
+ // process the change records in the order without having to worry about
+ // forward dependencies. But since deletions come before reparent
+ // operations, a delete may temporarily orphan a node that is
+ // updated later in the list.
+ virtual void OnChangesApplied(const BaseTransaction* trans,
+ const ChangeRecord* changes,
+ int change_count) = 0;
+
+ // A round-trip sync-cycle took place and the syncer has resolved any
+ // conflicts that may have arisen. This is kept separate from
+ // OnStatusChanged as there isn't really any state update; it is plainly
+ // a notification of a state transition.
+ virtual void OnSyncCycleCompleted() = 0;
+
+ // Called when user interaction may be required due to an auth problem.
+ virtual void OnAuthProblem(AuthProblem auth_problem) = 0;
+
+ // Called when initialization is complete to the point that SyncManager can
+ // process changes. This does not necessarily mean authentication succeeded
+ // or that the SyncManager is online.
+ // IMPORTANT: Creating any type of transaction before receiving this
+ // notification is illegal!
+ // WARNING: Calling methods on the SyncManager before receiving this
+ // message, unless otherwise specified, produces undefined behavior.
+ virtual void OnInitializationComplete() = 0;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(Observer);
+ };
+
+ // Create an uninitialized SyncManager. Callers must Init() before using.
+ SyncManager();
+ virtual ~SyncManager();
+
+ // Initialize the sync manager. |database_location| specifies the path of
+ // the directory in which to locate a sqlite repository storing the syncer
+ // backend state. Initialization will open the database, or create it if it
+ // does not already exist. Returns false on failure.
+ // |sync_server_and_path| and |sync_server_port| represent the Chrome sync
+ // server to use, and |use_ssl| specifies whether to communicate securely;
+ // the default is false.
+ // |gaia_service_id| is the service id used for GAIA authentication. If it's
+ // null then default will be used.
+ // |post_factory| will be owned internally and used to create
+ // instances of an HttpPostProvider.
+ // |auth_post_factory| will be owned internally and used to create
+ // instances of an HttpPostProvider for communicating with GAIA.
+ // TODO(timsteele): It seems like one factory should suffice, but for now to
+ // avoid having to deal with threading issues since the auth code and syncer
+ // code live on separate threads that run simultaneously, we just dedicate
+ // one to each component. Long term we may want to reconsider the HttpBridge
+ // API to take all the params in one chunk in a threadsafe manner.. which is
+ // still suboptimal as there will be high contention between the two threads
+ // on startup; so maybe what we have now is the best solution- it does mirror
+ // the CURL implementation as each thread creates their own internet handle.
+ // Investigate.
+ // |model_safe_worker| ownership is given to the SyncManager.
+ // |user_agent| is a 7-bit ASCII string suitable for use as the User-Agent
+ // HTTP header. Used internally when collecting stats to classify clients.
+ bool Init(const sync_char16* database_location,
+ const char* sync_server_and_path,
+ int sync_server_port,
+ const char* gaia_service_id,
+ const char* gaia_source,
+ bool use_ssl,
+ HttpPostProviderFactory* post_factory,
+ HttpPostProviderFactory* auth_post_factory,
+ ModelSafeWorkerInterface* model_safe_worker,
+ bool attempt_last_user_authentication,
+ const char* user_agent);
+
+ // Returns the username last used for a successful authentication as a
+ // null-terminated string. Returns empty if there is no such username.
+ // The memory is not owned by the caller and should be copied.
+ const char* GetAuthenticatedUsername();
+
+ // Submit credentials to GAIA for verification and start the
+ // syncing process on success. On success, both |username| and the obtained
+ // auth token are persisted on disk for future re-use.
+ // If authentication fails, OnAuthProblem is called on our Observer.
+ // The Observer may, in turn, decide to try again with new
+ // credentials. Calling this method again is the appropriate course of action
+ // to "retry".
+ // |username| and |password| are expected to be owned by the caller.
+ void Authenticate(const char* username, const char* password);
+
+ // Adds a listener to be notified of sync events.
+ // NOTE: It is OK (in fact, it's probably a good idea) to call this before
+ // having received OnInitializationCompleted.
+ void SetObserver(Observer* observer);
+
+ // Remove the observer set by SetObserver (no op if none was set).
+ // Make sure to call this if the Observer set in SetObserver is being
+ // destroyed so the SyncManager doesn't potentially dereference garbage.
+ void RemoveObserver();
+
+ // Status-related getters. Typically GetStatusSummary will suffice, but
+ // GetDetailedSyncStatus can be useful for gathering debug-level details of
+ // the internals of the sync engine.
+ Status::Summary GetStatusSummary() const;
+ Status GetDetailedStatus() const;
+
+ // Get the internal implementation for use by BaseTransaction, etc.
+ SyncInternal* GetImpl() const;
+
+ // Call periodically from a database-safe thread to persist recent changes
+ // to the syncapi model.
+ void SaveChanges();
+
+ // Invoking this method will result in the syncapi bypassing authentication
+ // and opening a local store suitable for testing client code. When in this
+ // mode, nothing will ever get synced to a server (in fact no HTTP
+ // communication will take place).
+ // Note: The SyncManager precondition that you must first call Init holds;
+ // this will fail unless we're initialized.
+ void SetupForTestMode(const sync_char16* test_username);
+
+ // Issue a final SaveChanges, close sqlite handles, and stop running threads.
+ // Must be called from the same thread that called Init().
+ void Shutdown();
+
+ UserShare* GetUserShare() const;
+
+ private:
+ // An opaque pointer to the nested private class.
+ SyncInternal* data_;
+
+ DISALLOW_COPY_AND_ASSIGN(SyncManager);
+};
+
+// An interface the embedding application (e.g. Chromium) implements to
+// provide required HTTP POST functionality to the syncer backend.
+// This interface is designed for one-time use. You create one, use it, and
+// create another if you want to make a subsequent POST.
+// TODO(timsteele): Bug 1482576. Consider splitting syncapi.h into two files:
+// one for the API defining the exports, which doesn't need to be included from
+// anywhere internally, and another file for the interfaces like this one.
+class HttpPostProviderInterface {
+ public:
+ HttpPostProviderInterface() { }
+ virtual ~HttpPostProviderInterface() { }
+
+ // Use specified user agent string when POSTing. If not called a default UA
+ // may be used.
+ virtual void SetUserAgent(const char* user_agent) = 0;
+
+ // Set the URL to POST to.
+ virtual void SetURL(const char* url, int port) = 0;
+
+ // Set the type, length and content of the POST payload.
+ // |content_type| is a null-terminated MIME type specifier.
+ // |content| is a data buffer; Do not interpret as a null-terminated string.
+ // |content_length| is the total number of chars in |content|. It is used to
+ // assign/copy |content| data.
+ virtual void SetPostPayload(const char* content_type, int content_length,
+ const char* content) = 0;
+
+ // Add the specified cookie to the request context using the url set by
+ // SetURL as the key. |cookie| should be a standard cookie line
+ // [e.g "name=val; name2=val2"]. |cookie| should be copied.
+ virtual void AddCookieForRequest(const char* cookie) = 0;
+
+ // Returns true if the URL request succeeded. If the request failed,
+ // os_error() may be non-zero and hence contain more information.
+ virtual bool MakeSynchronousPost(int* os_error_code, int* response_code) = 0;
+
+ // Get the length of the content returned in the HTTP response.
+ // This does not count the trailing null-terminating character returned
+ // by GetResponseContent, so it is analogous to calling string.length.
+ virtual int GetResponseContentLength() const = 0;
+
+ // Get the content returned in the HTTP response.
+ // This is a null terminated string of characters.
+ // Value should be copied.
+ virtual const char* GetResponseContent() const = 0;
+
+ // To simplify passing a vector<string> across this API, we provide the
+ // following two methods. Use GetResponseCookieCount to bound a loop calling
+ // GetResponseCookieAt once for each integer in the range
+ // [0, GetNumCookiesInResponse). The char* returned should be copied.
+ virtual int GetResponseCookieCount() const = 0;
+ virtual const char* GetResponseCookieAt(int cookie_number) const = 0;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(HttpPostProviderInterface);
+};
+
+// A factory to create HttpPostProviders to hide details about the
+// implementations and dependencies.
+// A factory instance itself should be owned by whomever uses it to create
+// HttpPostProviders.
+class HttpPostProviderFactory {
+ public:
+ // Obtain a new HttpPostProviderInterface instance, owned by caller.
+ virtual HttpPostProviderInterface* Create() = 0;
+
+ // When the interface is no longer needed (ready to be cleaned up), clients
+ // must call Destroy().
+ // This allows actual HttpPostProvider subclass implementations to be
+ // reference counted, which is useful if a particular implementation uses
+ // multiple threads to serve network requests.
+ virtual void Destroy(HttpPostProviderInterface* http) = 0;
+ virtual ~HttpPostProviderFactory() { }
+};
+
+// A class syncapi clients should use whenever the underlying model is bound to
+// a particular thread in the embedding application. This exposes an interface
+// by which any model-modifying invocations will be forwarded to the
+// appropriate thread in the embedding application.
+// "model safe" refers to not allowing an embedding application model to fall
+// out of sync with the syncable::Directory due to race conditions.
+class ModelSafeWorkerInterface {
+ public:
+ virtual ~ModelSafeWorkerInterface() { }
+ // A Visitor is passed to CallDoWorkFromModelSafeThreadAndWait invocations,
+ // and it's sole purpose is to provide a way for the ModelSafeWorkerInterface
+ // implementation to actually _do_ the work required, by calling the only
+ // method on this class, DoWork().
+ class Visitor {
+ public:
+ virtual ~Visitor() { }
+ // When on a model safe thread, this should be called to have the syncapi
+ // actually perform the work needing to be done.
+ virtual void DoWork() = 0;
+ };
+ // Subclasses should implement to invoke DoWork on |visitor| once on a thread
+ // appropriate for data model modifications.
+ // While it doesn't hurt, the impl does not need to be re-entrant (for now).
+ // Note: |visitor| is owned by caller.
+ virtual void CallDoWorkFromModelSafeThreadAndWait(Visitor* visitor) = 0;
+};
+
+} // namespace sync_api
+
+#endif // CHROME_BROWSER_SYNC_ENGINE_SYNCAPI_H_
diff --git a/chrome/browser/sync/glue/bookmark_model_worker.cc b/chrome/browser/sync/glue/bookmark_model_worker.cc
new file mode 100644
index 0000000..49fba9a
--- /dev/null
+++ b/chrome/browser/sync/glue/bookmark_model_worker.cc
@@ -0,0 +1,114 @@
+// Copyright (c) 2006-2008 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#include "chrome/browser/sync/glue/bookmark_model_worker.h"
+
+#include "base/message_loop.h"
+#include "base/waitable_event.h"
+
+namespace browser_sync {
+
+void BookmarkModelWorker::CallDoWorkFromModelSafeThreadAndWait(
+ ModelSafeWorkerInterface::Visitor* visitor) {
+ // It is possible this gets called when we are in the STOPPING state, because
+ // the UI loop has initiated shutdown but the syncer hasn't got the memo yet.
+ // This is fine, the work will get scheduled and run normally or run by our
+ // code handling this case in Stop().
+ DCHECK_NE(state_, STOPPED);
+ if (state_ == STOPPED)
+ return;
+ if (MessageLoop::current() == bookmark_model_loop_) {
+ DLOG(WARNING) << "CallDoWorkFromModelSafeThreadAndWait called from "
+ << "bookmark_model_loop_. Probably a nested invocation?";
+ visitor->DoWork();
+ return;
+ }
+
+ // Create an unsignaled event to wait on.
+ base::WaitableEvent work_done(false, false);
+ {
+ // We lock only to avoid PostTask'ing a NULL pending_work_ (because it
+ // could get Run() in Stop() and call OnTaskCompleted before we post).
+ // The task is owned by the message loop as per usual.
+ AutoLock lock(pending_work_lock_);
+ DCHECK(!pending_work_);
+ pending_work_ = new CallDoWorkAndSignalTask(visitor, &work_done, this);
+ bookmark_model_loop_->PostTask(FROM_HERE, pending_work_);
+ }
+ syncapi_event_.Signal(); // Notify that the syncapi produced work for us.
+ work_done.Wait();
+}
+
+BookmarkModelWorker::~BookmarkModelWorker() {
+ DCHECK_EQ(state_, STOPPED);
+}
+
+void BookmarkModelWorker::OnSyncerShutdownComplete() {
+ // The SyncerThread has terminated and we are no longer needed by syncapi.
+ // The UI loop initiated shutdown and is (or will be) waiting in Stop().
+ // We could either be WORKING or RUNNING_MANUAL_SHUTDOWN_PUMP, depending
+ // on where we timeslice the UI thread in Stop; but we can't be STOPPED,
+ // because that would imply NotifySyncapiShutdownComplete already signaled.
+ DCHECK_NE(state_, STOPPED);
+
+ syncapi_has_shutdown_ = true;
+ syncapi_event_.Signal();
+}
+
+void BookmarkModelWorker::Stop() {
+ DCHECK_EQ(MessageLoop::current(), bookmark_model_loop_);
+ DCHECK_EQ(state_, WORKING);
+
+ // We're on our own now, the beloved UI MessageLoop is no longer running.
+ // Any tasks scheduled or to be scheduled on the UI MessageLoop will not run.
+ state_ = RUNNING_MANUAL_SHUTDOWN_PUMP;
+
+ // Drain any final task manually until the SyncerThread tells us it has
+ // totally finished. Note we use a 'while' loop and not 'if'. The main subtle
+ // reason for this is that syncapi_event could be signaled the first time we
+ // come through due to an old CallDoWork call, and we need to keep looping
+ // until the SyncerThread either calls it again or tells us it is done. There
+ // should only ever be 0 or 1 tasks Run() here, however.
+ while (!syncapi_has_shutdown_) {
+ {
+ AutoLock lock(pending_work_lock_);
+ if (pending_work_)
+ pending_work_->Run();
+ }
+ syncapi_event_.Wait(); // Signaled either by new task, or SyncerThread
+ // termination.
+ }
+
+ state_ = STOPPED;
+}
+
+void BookmarkModelWorker::CallDoWorkAndSignalTask::Run() {
+ if (!visitor_) {
+ // This can happen during tests or cases where there are more than just the
+ // default BookmarkModelWorker in existence and it gets destroyed before
+ // the main UI loop has terminated. There is no easy way to assert the
+ // loop is running / not running at the moment, so we just provide cancel
+ // semantics here and short-circuit.
+ // TODO(timsteele): Maybe we should have the message loop destruction
+ // observer fire when the loop has ended, just a bit before it
+ // actually gets destroyed.
+ return;
+ }
+ visitor_->DoWork();
+
+ // Sever ties with visitor_ to allow the sanity-checking above that we don't
+ // get run twice.
+ visitor_ = NULL;
+
+ // Notify the BookmarkModelWorker that scheduled us that we have run
+ // successfully.
+ scheduler_->OnTaskCompleted();
+ work_done_->Signal(); // Unblock the syncer thread that scheduled us.
+}
+
+} // namespace browser_sync
+
+#endif // CHROME_PERSONALIZATION \ No newline at end of file
diff --git a/chrome/browser/sync/glue/bookmark_model_worker.h b/chrome/browser/sync/glue/bookmark_model_worker.h
new file mode 100644
index 0000000..9cd67c6
--- /dev/null
+++ b/chrome/browser/sync/glue/bookmark_model_worker.h
@@ -0,0 +1,134 @@
+// Copyright (c) 2006-2008 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#ifndef CHROME_BROWSER_SYNC_GLUE_BOOKMARK_MODEL_WORKER_H_
+#define CHROME_BROWSER_SYNC_GLUE_BOOKMARK_MODEL_WORKER_H_
+
+#include "base/lock.h"
+#include "base/task.h"
+#include "base/waitable_event.h"
+#include "chrome/browser/sync/engine/syncapi.h"
+
+class MessageLoop;
+
+namespace browser_sync {
+
+// A ModelSafeWorker for bookmarks that accepts work requests from the syncapi
+// that need to be fulfilled from the MessageLoop home to the BookmarkModel
+// (this is typically the "main" UI thread).
+//
+// Lifetime note: Instances of this class will generally be owned by the
+// SyncerThread. When the SyncerThread _object_ is destroyed, the
+// BookmarkModelWorker will be destroyed. The SyncerThread object is destroyed
+// after the actual syncer pthread has exited.
+class BookmarkModelWorker
+ : public sync_api::ModelSafeWorkerInterface {
+ public:
+ explicit BookmarkModelWorker(MessageLoop* bookmark_model_loop)
+ : state_(WORKING),
+ pending_work_(NULL),
+ syncapi_has_shutdown_(false),
+ bookmark_model_loop_(bookmark_model_loop),
+ syncapi_event_(false, false) {
+ }
+ virtual ~BookmarkModelWorker();
+
+ // A simple task to signal a waitable event after calling DoWork on a visitor.
+ class CallDoWorkAndSignalTask : public Task {
+ public:
+ CallDoWorkAndSignalTask(ModelSafeWorkerInterface::Visitor* visitor,
+ base::WaitableEvent* work_done,
+ BookmarkModelWorker* scheduler)
+ : visitor_(visitor), work_done_(work_done), scheduler_(scheduler) {
+ }
+ virtual ~CallDoWorkAndSignalTask() { }
+
+ // Task implementation.
+ virtual void Run();
+
+ private:
+ // Task data - a visitor that knows how to DoWork, and a waitable event
+ // to signal after the work has been done.
+ ModelSafeWorkerInterface::Visitor* visitor_;
+ base::WaitableEvent* work_done_;
+
+ // The BookmarkModelWorker responsible for scheduling us.
+ BookmarkModelWorker* const scheduler_;
+
+ DISALLOW_COPY_AND_ASSIGN(CallDoWorkAndSignalTask);
+ };
+
+ // Called by the UI thread on shutdown of the sync service. Blocks until
+ // the BookmarkModelWorker has safely met termination conditions, namely that
+ // no task scheduled by CallDoWorkFromModelSafeThreadAndWait remains un-
+ // processed and that syncapi will not schedule any further work for us to do.
+ void Stop();
+
+ // ModelSafeWorkerInterface implementation. Called on syncapi SyncerThread.
+ virtual void CallDoWorkFromModelSafeThreadAndWait(
+ ModelSafeWorkerInterface::Visitor* visitor);
+
+ // Upon receiving this idempotent call, the ModelSafeWorkerInterface can
+ // assume no work will ever be scheduled again from now on. If it has any work
+ // that it has not yet completed, it must make sure to run it as soon as
+ // possible as the Syncer is trying to shut down. Called from the CoreThread.
+ void OnSyncerShutdownComplete();
+
+ // Callback from |pending_work_| to notify us that it has been run.
+ // Called on |bookmark_model_loop_|.
+ void OnTaskCompleted() { pending_work_ = NULL; }
+
+ private:
+ // The life-cycle of a BookmarkModelWorker in three states.
+ enum State {
+ // We hit the ground running in this state and remain until
+ // the UI loop calls Stop().
+ WORKING,
+ // Stop() sequence has been initiated, but we have not received word that
+ // the SyncerThread has terminated and doesn't need us anymore. Since the
+ // UI MessageLoop is not running at this point, we manually process any
+ // last pending_task_ that the Syncer throws at us, effectively dedicating
+ // the UI thread to terminating the Syncer.
+ RUNNING_MANUAL_SHUTDOWN_PUMP,
+ // We have come to a complete stop, no scheduled work remains, and no work
+ // will be scheduled from now until our destruction.
+ STOPPED,
+ };
+
+ // This is set by the UI thread, but is not explicitly thread safe, so only
+ // read this value from other threads when you know it is absolutely safe (e.g
+ // there is _no_ way we can be in CallDoWork with state_ = STOPPED, so it is
+ // safe to read / compare in this case).
+ State state_;
+
+ // We keep a reference to any task we have scheduled so we can gracefully
+ // force them to run if the syncer is trying to shutdown.
+ Task* pending_work_;
+ Lock pending_work_lock_;
+
+ // Set by the SyncCoreThread when Syncapi shutdown has completed and the
+ // SyncerThread has terminated, so no more work will be scheduled. Read by
+ // the UI thread in Stop().
+ bool syncapi_has_shutdown_;
+
+ // The BookmarkModel's home-sweet-home MessageLoop.
+ MessageLoop* const bookmark_model_loop_;
+
+ // Used as a barrier at shutdown to ensure the SyncerThread terminates before
+ // we allow the UI thread to return from Stop(). This gets signalled whenever
+ // one of two events occur: a new pending_work_ task was scheduled, or the
+ // SyncerThread has terminated. We only care about (1) when we are in Stop(),
+ // because we have to manually Run() the task.
+ base::WaitableEvent syncapi_event_;
+
+ DISALLOW_COPY_AND_ASSIGN(BookmarkModelWorker);
+};
+
+} // namespace browser_sync
+
+#endif // CHROME_BROWSER_SYNC_GLUE_BOOKMARK_MODEL_WORKER_H_
+
+#endif // CHROME_PERSONALIZATION \ No newline at end of file
diff --git a/chrome/browser/sync/glue/bookmark_model_worker_unittest.cc b/chrome/browser/sync/glue/bookmark_model_worker_unittest.cc
new file mode 100644
index 0000000..5ef5b5ea6
--- /dev/null
+++ b/chrome/browser/sync/glue/bookmark_model_worker_unittest.cc
@@ -0,0 +1,224 @@
+// Copyright (c) 2006-2009 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.
+#ifdef CHROME_PERSONALIZATION
+
+#include "base/thread.h"
+#include "chrome/browser/sync/engine/syncapi.h"
+#include "chrome/browser/sync/glue/bookmark_model_worker.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using browser_sync::BookmarkModelWorker;
+using namespace sync_api;
+
+// Various boilerplate, primarily for the StopWithPendingWork test.
+
+class BookmarkModelWorkerVisitor : public ModelSafeWorkerInterface::Visitor {
+ public:
+ BookmarkModelWorkerVisitor(MessageLoop* faux_ui_loop,
+ base::WaitableEvent* was_run,
+ bool quit_loop)
+ : faux_ui_loop_(faux_ui_loop), quit_loop_when_run_(quit_loop),
+ was_run_(was_run) { }
+ virtual ~BookmarkModelWorkerVisitor() { }
+
+ virtual void DoWork() {
+ EXPECT_EQ(MessageLoop::current(), faux_ui_loop_);
+ was_run_->Signal();
+ if (quit_loop_when_run_)
+ MessageLoop::current()->Quit();
+ }
+
+ private:
+ MessageLoop* faux_ui_loop_;
+ bool quit_loop_when_run_;
+ base::WaitableEvent* was_run_;
+ DISALLOW_COPY_AND_ASSIGN(BookmarkModelWorkerVisitor);
+};
+
+// A faux-syncer that only interacts with its model safe worker.
+class Syncer {
+ public:
+ explicit Syncer(BookmarkModelWorker* worker) : worker_(worker) {}
+ ~Syncer() {}
+
+ void SyncShare(BookmarkModelWorkerVisitor* visitor) {
+ worker_->CallDoWorkFromModelSafeThreadAndWait(visitor);
+ }
+ private:
+ BookmarkModelWorker* worker_;
+ DISALLOW_COPY_AND_ASSIGN(Syncer);
+};
+
+// A task run from the SyncerThread to "sync share", ie tell the Syncer to
+// ask it's ModelSafeWorker to do something.
+class FakeSyncShareTask : public Task {
+ public:
+ FakeSyncShareTask(Syncer* syncer, BookmarkModelWorkerVisitor* visitor)
+ : syncer_(syncer), visitor_(visitor) {
+ }
+ virtual void Run() {
+ syncer_->SyncShare(visitor_);
+ }
+ private:
+ Syncer* syncer_;
+ BookmarkModelWorkerVisitor* visitor_;
+ DISALLOW_COPY_AND_ASSIGN(FakeSyncShareTask);
+};
+
+// A task run from the CoreThread to simulate terminating syncapi.
+class FakeSyncapiShutdownTask : public Task {
+ public:
+ FakeSyncapiShutdownTask(base::Thread* syncer_thread,
+ BookmarkModelWorker* worker,
+ base::WaitableEvent** jobs,
+ size_t job_count)
+ : syncer_thread_(syncer_thread), worker_(worker), jobs_(jobs),
+ job_count_(job_count), all_jobs_done_(false, false) { }
+ virtual void Run() {
+ // In real life, we would try and close a sync directory, which would
+ // result in the syncer calling it's own destructor, which results in
+ // the SyncerThread::HaltSyncer being called, which sets the
+ // syncer in RequestEarlyExit mode and waits until the Syncer finishes
+ // SyncShare to remove the syncer from it's watch. Here we just manually
+ // wait until all outstanding jobs are done to simulate what happens in
+ // SyncerThread::HaltSyncer.
+ all_jobs_done_.WaitMany(jobs_, job_count_);
+
+ // These two calls are made from SyncBackendHost::Core::DoShutdown.
+ syncer_thread_->Stop();
+ worker_->OnSyncerShutdownComplete();
+ }
+ private:
+ base::Thread* syncer_thread_;
+ BookmarkModelWorker* worker_;
+ base::WaitableEvent** jobs_;
+ size_t job_count_;
+ base::WaitableEvent all_jobs_done_;
+ DISALLOW_COPY_AND_ASSIGN(FakeSyncapiShutdownTask);
+};
+
+class BookmarkModelWorkerTest : public testing::Test {
+ public:
+ BookmarkModelWorkerTest() : faux_syncer_thread_("FauxSyncerThread"),
+ faux_core_thread_("FauxCoreThread") { }
+
+ virtual void SetUp() {
+ faux_syncer_thread_.Start();
+ bmw_.reset(new BookmarkModelWorker(&faux_ui_loop_));
+ syncer_.reset(new Syncer(bmw_.get()));
+ }
+
+ Syncer* syncer() { return syncer_.get(); }
+ BookmarkModelWorker* bmw() { return bmw_.get(); }
+ base::Thread* core_thread() { return &faux_core_thread_; }
+ base::Thread* syncer_thread() { return &faux_syncer_thread_; }
+ MessageLoop* ui_loop() { return &faux_ui_loop_; }
+ private:
+ MessageLoop faux_ui_loop_;
+ base::Thread faux_syncer_thread_;
+ base::Thread faux_core_thread_;
+ scoped_ptr<BookmarkModelWorker> bmw_;
+ scoped_ptr<Syncer> syncer_;
+};
+
+TEST_F(BookmarkModelWorkerTest, ScheduledWorkRunsOnUILoop) {
+ base::WaitableEvent v_was_run(false, false);
+ scoped_ptr<BookmarkModelWorkerVisitor> v(
+ new BookmarkModelWorkerVisitor(ui_loop(), &v_was_run, true));
+
+ syncer_thread()->message_loop()->PostTask(FROM_HERE,
+ new FakeSyncShareTask(syncer(), v.get()));
+
+ // We are on the UI thread, so run our loop to process the
+ // (hopefully) scheduled task from a SyncShare invocation.
+ MessageLoop::current()->Run();
+
+ bmw()->OnSyncerShutdownComplete();
+ bmw()->Stop();
+ syncer_thread()->Stop();
+}
+
+TEST_F(BookmarkModelWorkerTest, StopWithPendingWork) {
+ // What we want to set up is the following:
+ // ("ui_thread" is the thread we are currently executing on)
+ // 1 - simulate the user shutting down the browser, and the ui thread needing
+ // to terminate the core thread.
+ // 2 - the core thread is where the syncapi is accessed from, and so it needs
+ // to shut down the SyncerThread.
+ // 3 - the syncer is waiting on the BookmarkModelWorker to
+ // perform a task for it.
+ // The BookmarkModelWorker's manual shutdown pump will save the day, as the
+ // UI thread is not actually trying to join() the core thread, it is merely
+ // waiting for the SyncerThread to give it work or to finish. After that, it
+ // will join the core thread which should succeed as the SyncerThread has left
+ // the building. Unfortunately this test as written is not provably decidable,
+ // as it will always halt on success, but it may not on failure (namely if
+ // the task scheduled by the Syncer is _never_ run).
+ core_thread()->Start();
+ base::WaitableEvent v_ran(false, false);
+ scoped_ptr<BookmarkModelWorkerVisitor> v(new BookmarkModelWorkerVisitor(
+ ui_loop(), &v_ran, false));
+ base::WaitableEvent* jobs[] = { &v_ran };
+
+ // The current message loop is not running, so queue a task to cause
+ // BookmarkModelWorker::Stop() to play a crucial role. See comment below.
+ syncer_thread()->message_loop()->PostTask(FROM_HERE,
+ new FakeSyncShareTask(syncer(), v.get()));
+
+ // This is what gets the core_thread blocked on the syncer_thread.
+ core_thread()->message_loop()->PostTask(FROM_HERE,
+ new FakeSyncapiShutdownTask(syncer_thread(), bmw(), jobs, 1));
+
+ // This is what gets the UI thread blocked until NotifyExitRequested,
+ // which is called when FakeSyncapiShutdownTask runs and deletes the syncer.
+ bmw()->Stop();
+
+ EXPECT_FALSE(syncer_thread()->IsRunning());
+ core_thread()->Stop();
+}
+
+TEST_F(BookmarkModelWorkerTest, HypotheticalManualPumpFlooding) {
+ // This situation should not happen in real life because the Syncer should
+ // never send more than one CallDoWork notification after early_exit_requested
+ // has been set, but our BookmarkModelWorker is built to handle this case
+ // nonetheless. It may be needed in the future, and since we support it and
+ // it is not actually exercised in the wild this test is essential.
+ // It is identical to above except we schedule more than one visitor.
+ core_thread()->Start();
+
+ // Our ammunition.
+ base::WaitableEvent fox1_ran(false, false);
+ scoped_ptr<BookmarkModelWorkerVisitor> fox1(new BookmarkModelWorkerVisitor(
+ ui_loop(), &fox1_ran, false));
+ base::WaitableEvent fox2_ran(false, false);
+ scoped_ptr<BookmarkModelWorkerVisitor> fox2(new BookmarkModelWorkerVisitor(
+ ui_loop(), &fox2_ran, false));
+ base::WaitableEvent fox3_ran(false, false);
+ scoped_ptr<BookmarkModelWorkerVisitor> fox3(new BookmarkModelWorkerVisitor(
+ ui_loop(), &fox3_ran, false));
+ base::WaitableEvent* jobs[] = { &fox1_ran, &fox2_ran, &fox3_ran };
+
+ // The current message loop is not running, so queue a task to cause
+ // BookmarkModelWorker::Stop() to play a crucial role. See comment below.
+ syncer_thread()->message_loop()->PostTask(FROM_HERE,
+ new FakeSyncShareTask(syncer(), fox1.get()));
+ syncer_thread()->message_loop()->PostTask(FROM_HERE,
+ new FakeSyncShareTask(syncer(), fox2.get()));
+
+ // This is what gets the core_thread blocked on the syncer_thread.
+ core_thread()->message_loop()->PostTask(FROM_HERE,
+ new FakeSyncapiShutdownTask(syncer_thread(), bmw(), jobs, 3));
+ syncer_thread()->message_loop()->PostTask(FROM_HERE,
+ new FakeSyncShareTask(syncer(), fox3.get()));
+
+ // This is what gets the UI thread blocked until NotifyExitRequested,
+ // which is called when FakeSyncapiShutdownTask runs and deletes the syncer.
+ bmw()->Stop();
+
+ // Was the thread killed?
+ EXPECT_FALSE(syncer_thread()->IsRunning());
+ core_thread()->Stop();
+}
+
+#endif // CHROME_PERSONALIZATION \ No newline at end of file
diff --git a/chrome/browser/sync/glue/http_bridge.cc b/chrome/browser/sync/glue/http_bridge.cc
new file mode 100644
index 0000000..afbbc97
--- /dev/null
+++ b/chrome/browser/sync/glue/http_bridge.cc
@@ -0,0 +1,252 @@
+// Copyright (c) 2006-2008 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#include "chrome/browser/sync/glue/http_bridge.h"
+
+#include "base/message_loop.h"
+#include "base/string_util.h"
+#include "chrome/browser/chrome_thread.h"
+#include "chrome/browser/profile.h"
+#include "net/base/cookie_monster.h"
+#include "net/base/load_flags.h"
+#include "net/http/http_network_layer.h"
+#include "net/proxy/proxy_service.h"
+#include "net/url_request/url_request_status.h"
+#include "webkit/glue/webkit_glue.h"
+
+namespace browser_sync {
+
+HttpBridge::RequestContext* HttpBridgeFactory::GetRequestContext() {
+ if (!request_context_) {
+ request_context_ =
+ new HttpBridge::RequestContext(Profile::GetDefaultRequestContext());
+ request_context_->AddRef();
+ }
+ return request_context_;
+}
+
+HttpBridgeFactory::~HttpBridgeFactory() {
+ if (request_context_) {
+ // Clean up request context on IO thread.
+ ChromeThread::GetMessageLoop(ChromeThread::IO)->ReleaseSoon(FROM_HERE,
+ request_context_);
+ request_context_ = NULL;
+ }
+}
+
+sync_api::HttpPostProviderInterface* HttpBridgeFactory::Create() {
+ // TODO(timsteele): We want the active profile request context.
+ HttpBridge* http = new HttpBridge(GetRequestContext(),
+ ChromeThread::GetMessageLoop(ChromeThread::IO));
+ http->AddRef();
+ return http;
+}
+
+void HttpBridgeFactory::Destroy(sync_api::HttpPostProviderInterface* http) {
+ static_cast<HttpBridge*>(http)->Release();
+}
+
+HttpBridge::RequestContext::RequestContext(
+ const URLRequestContext* baseline_context) {
+
+ // Create empty, in-memory cookie store.
+ cookie_store_ = new net::CookieMonster();
+
+ // We don't use a cache for bridged loads, but we do want to share proxy info.
+ host_resolver_ = baseline_context->host_resolver();
+ proxy_service_ = baseline_context->proxy_service();
+ http_transaction_factory_ =
+ net::HttpNetworkLayer::CreateFactory(host_resolver_, proxy_service_);
+
+ // TODO(timsteele): We don't currently listen for pref changes of these
+ // fields or CookiePolicy; I'm not sure we want to strictly follow the
+ // default settings, since for example if the user chooses to block all
+ // cookies, sync will start failing. Also it seems like accept_lang/charset
+ // should be tied to whatever the sync servers expect (if anything). These
+ // fields should probably just be settable by sync backend; though we should
+ // figure out if we need to give the user explicit control over policies etc.
+ accept_language_ = baseline_context->accept_language();
+ accept_charset_ = baseline_context->accept_charset();
+
+ // We default to the browser's user agent. This can (and should) be overridden
+ // with set_user_agent.
+ user_agent_ = webkit_glue::GetUserAgent(GURL());
+}
+
+HttpBridge::RequestContext::~RequestContext() {
+ delete cookie_store_;
+ delete http_transaction_factory_;
+}
+
+HttpBridge::HttpBridge(HttpBridge::RequestContext* context,
+ MessageLoop* io_loop)
+ : context_for_request_(context),
+ url_poster_(NULL),
+ created_on_loop_(MessageLoop::current()),
+ io_loop_(io_loop),
+ request_completed_(false),
+ request_succeeded_(false),
+ http_response_code_(-1),
+ http_post_completed_(false, false),
+ use_io_loop_for_testing_(false) {
+ context_for_request_->AddRef();
+}
+
+HttpBridge::~HttpBridge() {
+ io_loop_->ReleaseSoon(FROM_HERE, context_for_request_);
+}
+
+void HttpBridge::SetUserAgent(const char* user_agent) {
+ DCHECK_EQ(MessageLoop::current(), created_on_loop_);
+ DCHECK(!request_completed_);
+ context_for_request_->set_user_agent(user_agent);
+}
+
+void HttpBridge::SetURL(const char* url, int port) {
+ DCHECK_EQ(MessageLoop::current(), created_on_loop_);
+ DCHECK(!request_completed_);
+ DCHECK(url_for_request_.is_empty())
+ << "HttpBridge::SetURL called more than once?!";
+ GURL temp(url);
+ GURL::Replacements replacements;
+ std::string port_str = IntToString(port);
+ replacements.SetPort(port_str.c_str(),
+ url_parse::Component(0, port_str.length()));
+ url_for_request_ = temp.ReplaceComponents(replacements);
+}
+
+void HttpBridge::SetPostPayload(const char* content_type,
+ int content_length,
+ const char* content) {
+ DCHECK_EQ(MessageLoop::current(), created_on_loop_);
+ DCHECK(!request_completed_);
+ DCHECK(content_type_.empty()) << "Bridge payload already set.";
+ DCHECK_GE(content_length, 0) << "Content length < 0";
+ content_type_ = content_type;
+ if (!content || (content_length == 0)) {
+ DCHECK_EQ(content_length, 0);
+ request_content_ = " "; // TODO(timsteele): URLFetcher requires non-empty
+ // content for POSTs whereas CURL does not, for now
+ // we hack this to support the sync backend.
+ } else {
+ request_content_.assign(content, content_length);
+ }
+}
+
+void HttpBridge::AddCookieForRequest(const char* cookie) {
+ DCHECK_EQ(MessageLoop::current(), created_on_loop_);
+ DCHECK(!request_completed_);
+ DCHECK(url_for_request_.is_valid()) << "Valid URL not set.";
+ if (!url_for_request_.is_valid()) return;
+
+ if (!context_for_request_->cookie_store()->SetCookie(url_for_request_,
+ cookie)) {
+ DLOG(WARNING) << "Cookie " << cookie
+ << " could not be added for url: " << url_for_request_ << ".";
+ }
+}
+
+bool HttpBridge::MakeSynchronousPost(int* os_error_code, int* response_code) {
+ DCHECK_EQ(MessageLoop::current(), created_on_loop_);
+ DCHECK(!request_completed_);
+ DCHECK(url_for_request_.is_valid()) << "Invalid URL for request";
+ DCHECK(!content_type_.empty()) << "Payload not set";
+ DCHECK(context_for_request_->is_user_agent_set()) << "User agent not set";
+
+ io_loop_->PostTask(FROM_HERE, NewRunnableMethod(this,
+ &HttpBridge::CallMakeAsynchronousPost));
+
+ if (!http_post_completed_.Wait()) // Block until network request completes.
+ NOTREACHED(); // See OnURLFetchComplete.
+
+ DCHECK(request_completed_);
+ *os_error_code = os_error_code_;
+ *response_code = http_response_code_;
+ return request_succeeded_;
+}
+
+void HttpBridge::MakeAsynchronousPost() {
+ DCHECK_EQ(MessageLoop::current(), io_loop_);
+ DCHECK(!request_completed_);
+
+ url_poster_ = new URLFetcher(url_for_request_, URLFetcher::POST, this);
+ url_poster_->set_request_context(context_for_request_);
+ url_poster_->set_upload_data(content_type_, request_content_);
+
+ if (use_io_loop_for_testing_)
+ url_poster_->set_io_loop(io_loop_);
+
+ url_poster_->Start();
+}
+
+int HttpBridge::GetResponseContentLength() const {
+ DCHECK_EQ(MessageLoop::current(), created_on_loop_);
+ DCHECK(request_completed_);
+ return response_content_.size();
+}
+
+const char* HttpBridge::GetResponseContent() const {
+ DCHECK_EQ(MessageLoop::current(), created_on_loop_);
+ DCHECK(request_completed_);
+ return response_content_.c_str();
+}
+
+int HttpBridge::GetResponseCookieCount() const {
+ DCHECK_EQ(MessageLoop::current(), created_on_loop_);
+ DCHECK(request_completed_);
+ return response_cookies_.size();
+}
+
+const char* HttpBridge::GetResponseCookieAt(int cookie_number) const {
+ DCHECK_EQ(MessageLoop::current(), created_on_loop_);
+ DCHECK(request_completed_);
+ bool valid_number = (cookie_number >= 0) &&
+ (static_cast<size_t>(cookie_number) < response_cookies_.size());
+ DCHECK(valid_number);
+ if (!valid_number)
+ return NULL;
+ return response_cookies_[cookie_number].c_str();
+}
+
+void HttpBridge::OnURLFetchComplete(const URLFetcher *source, const GURL &url,
+ const URLRequestStatus &status,
+ int response_code,
+ const ResponseCookies &cookies,
+ const std::string &data) {
+ DCHECK_EQ(MessageLoop::current(), io_loop_);
+
+ request_completed_ = true;
+ request_succeeded_ = (URLRequestStatus::SUCCESS == status.status());
+ http_response_code_ = response_code;
+ os_error_code_ = status.os_error();
+
+ // TODO(timsteele): For now we need this "fixup" to match up with what the
+ // sync backend expects. This seems to be non-standard and shouldn't be done
+ // here in HttpBridge, and it breaks part of the unittest.
+ for (size_t i = 0; i < cookies.size(); ++i) {
+ net::CookieMonster::ParsedCookie parsed_cookie(cookies[i]);
+ std::string cookie = " \t \t \t \t \t";
+ cookie += parsed_cookie.Name() + "\t";
+ cookie += parsed_cookie.Value();
+ response_cookies_.push_back(cookie);
+ }
+
+ response_content_ = data;
+
+ // End of the line for url_poster_. It lives only on the io_loop.
+ // We defer deletion because we're inside a callback from a component of the
+ // URLFetcher, so it seems most natural / "polite" to let the stack unwind.
+ io_loop_->DeleteSoon(FROM_HERE, url_poster_);
+ url_poster_ = NULL;
+
+ // Wake the blocked syncer thread in MakeSynchronousPost.
+ // WARNING: DONT DO ANYTHING AFTER THIS CALL! |this| may be deleted!
+ http_post_completed_.Signal();
+}
+
+} // namespace browser_sync
+
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/sync/glue/http_bridge.h b/chrome/browser/sync/glue/http_bridge.h
new file mode 100644
index 0000000..184a040
--- /dev/null
+++ b/chrome/browser/sync/glue/http_bridge.h
@@ -0,0 +1,173 @@
+// Copyright (c) 2006-2008 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#ifndef CHROME_BROWSER_SYNC_GLUE_HTTP_BRIDGE_H_
+#define CHROME_BROWSER_SYNC_GLUE_HTTP_BRIDGE_H_
+
+#include <string>
+
+#include "base/ref_counted.h"
+#include "base/waitable_event.h"
+#include "chrome/browser/net/url_fetcher.h"
+#include "chrome/browser/sync/engine/syncapi.h"
+#include "googleurl/src/gurl.h"
+#include "net/url_request/url_request_context.h"
+#include "testing/gtest/include/gtest/gtest_prod.h"
+
+class MessageLoop;
+class HttpBridgeTest;
+
+namespace browser_sync {
+
+// A bridge between the syncer and Chromium HTTP layers.
+// Provides a way for the sync backend to use Chromium directly for HTTP
+// requests rather than depending on a third party provider (e.g libcurl).
+// This is a one-time use bridge. Create one for each request you want to make.
+// It is RefCountedThreadSafe because it can PostTask to the io loop, and thus
+// needs to stick around across context switches, etc.
+class HttpBridge : public base::RefCountedThreadSafe<HttpBridge>,
+ public sync_api::HttpPostProviderInterface,
+ public URLFetcher::Delegate {
+ public:
+ // A request context used for HTTP requests bridged from the sync backend.
+ // A bridged RequestContext has a dedicated in-memory cookie store and does
+ // not use a cache. Thus the same type can be used for incognito mode.
+ // TODO(timsteele): We subclass here instead of add a factory method on
+ // ChromeURLRequestContext because:
+ // 1) we want the ability to set_user_agent
+ // 2) avoids ifdefs for now
+ // 3) not sure we want to strictly follow settings for cookie policy,
+ // accept lang/charset, since changing these could break syncing.
+ class RequestContext : public URLRequestContext {
+ public:
+ // |baseline_context| is used to obtain the accept-language,
+ // accept-charsets, and proxy service information for bridged requests.
+ // Typically |baseline_context| should be the URLRequestContext of the
+ // currently active profile.
+ explicit RequestContext(const URLRequestContext* baseline_context);
+ virtual ~RequestContext();
+
+ // Set the user agent for requests using this context. The default is
+ // the browser's UA string.
+ void set_user_agent(const std::string& ua) { user_agent_ = ua; }
+ bool is_user_agent_set() const { return !user_agent_.empty(); }
+
+ virtual const std::string& GetUserAgent(const GURL& url) const {
+ // If the user agent is set explicitly return that, otherwise call the
+ // base class method to return default value.
+ return user_agent_.empty() ?
+ URLRequestContext::GetUserAgent(url) : user_agent_;
+ }
+
+ private:
+ std::string user_agent_;
+
+ DISALLOW_COPY_AND_ASSIGN(RequestContext);
+ };
+
+ HttpBridge(RequestContext* context, MessageLoop* io_loop);
+ virtual ~HttpBridge();
+
+ // sync_api::HttpPostProvider implementation.
+ virtual void SetUserAgent(const char* user_agent);
+ virtual void SetURL(const char* url, int port);
+ virtual void SetPostPayload(const char* content_type, int content_length,
+ const char* content);
+ virtual void AddCookieForRequest(const char* cookie);
+ virtual bool MakeSynchronousPost(int* os_error_code, int* response_code);
+ virtual int GetResponseContentLength() const;
+ virtual const char* GetResponseContent() const;
+ virtual int GetResponseCookieCount() const;
+ virtual const char* GetResponseCookieAt(int cookie_number) const;
+
+ // URLFetcher::Delegate implementation.
+ virtual void OnURLFetchComplete(const URLFetcher* source, const GURL& url,
+ const URLRequestStatus& status,
+ int response_code,
+ const ResponseCookies& cookies,
+ const std::string& data);
+
+ protected:
+ // Protected virtual so the unit test can override to shunt network requests.
+ virtual void MakeAsynchronousPost();
+
+ private:
+ friend class ::HttpBridgeTest;
+
+ // Called on the io_loop_ to issue the network request. The extra level
+ // of indirection is so that the unit test can override this behavior but we
+ // still have a function to statically pass to PostTask.
+ void CallMakeAsynchronousPost() { MakeAsynchronousPost(); }
+
+ // A customized URLRequestContext for bridged requests. See RequestContext
+ // definition for details.
+ RequestContext* context_for_request_;
+
+ // Our hook into the network layer is a URLFetcher. USED ONLY ON THE IO LOOP,
+ // so we can block created_on_loop_ while the fetch is in progress.
+ // NOTE: This is not a scoped_ptr for a reason. It must be deleted on the same
+ // thread that created it, which isn't the same thread |this| gets deleted on.
+ // We must manually delete url_poster_ on the io_loop_.
+ URLFetcher* url_poster_;
+
+ // The message loop of the thread we were created on. This is the thread that
+ // will block on MakeSynchronousPost while the IO thread fetches data from
+ // the network.
+ // This should be the main syncer thread (SyncerThread) which is what blocks
+ // on network IO through curl_easy_perform.
+ MessageLoop* const created_on_loop_;
+
+ // Member variable for the IO loop instead of asking ChromeThread directly,
+ // done this way for testability.
+ MessageLoop* const io_loop_;
+
+ // The URL to POST to.
+ GURL url_for_request_;
+
+ // POST payload information.
+ std::string content_type_;
+ std::string request_content_;
+
+ // Cached response data.
+ bool request_completed_;
+ bool request_succeeded_;
+ int http_response_code_;
+ int os_error_code_;
+ ResponseCookies response_cookies_;
+ std::string response_content_;
+
+ // A waitable event we use to provide blocking semantics to
+ // MakeSynchronousPost. We block created_on_loop_ while the io_loop_ fetches
+ // network request.
+ base::WaitableEvent http_post_completed_;
+
+ // This is here so that the unit test subclass can force our URLFetcher to
+ // use the io_loop_ passed on construction for network requests, rather than
+ // ChromeThread::IO's message loop (which won't exist in testing).
+ bool use_io_loop_for_testing_;
+
+ DISALLOW_COPY_AND_ASSIGN(HttpBridge);
+};
+
+class HttpBridgeFactory
+ : public sync_api::HttpPostProviderFactory {
+ public:
+ HttpBridgeFactory() : request_context_(NULL) { }
+ virtual ~HttpBridgeFactory();
+ virtual sync_api::HttpPostProviderInterface* Create();
+ virtual void Destroy(sync_api::HttpPostProviderInterface* http);
+ private:
+ HttpBridge::RequestContext* GetRequestContext();
+ // We must Release() this from the IO thread.
+ HttpBridge::RequestContext* request_context_;
+ DISALLOW_COPY_AND_ASSIGN(HttpBridgeFactory);
+};
+
+} // namespace browser_sync
+
+#endif // CHROME_BROWSER_SYNC_GLUE_HTTP_BRIDGE_H_
+
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/sync/glue/http_bridge_unittest.cc b/chrome/browser/sync/glue/http_bridge_unittest.cc
new file mode 100644
index 0000000..06068ca
--- /dev/null
+++ b/chrome/browser/sync/glue/http_bridge_unittest.cc
@@ -0,0 +1,167 @@
+// Copyright (c) 2006-2008 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.
+#ifdef CHROME_PERSONALIZATION
+
+#include "base/thread.h"
+#include "chrome/browser/sync/glue/http_bridge.h"
+#include "net/url_request/url_request_unittest.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using browser_sync::HttpBridge;
+
+namespace {
+// TODO(timsteele): Should use PathService here. See Chromium Issue 3113.
+const char16 kDocRoot[] = L"chrome/test/data";
+}
+
+class HttpBridgeTest : public testing::Test {
+ public:
+ HttpBridgeTest() : io_thread_("HttpBridgeTest IO thread") {
+ }
+
+ virtual void SetUp() {
+ base::Thread::Options options;
+ options.message_loop_type = MessageLoop::TYPE_IO;
+ io_thread_.StartWithOptions(options);
+ }
+
+ virtual void TearDown() {
+ io_thread_.Stop();
+ }
+
+ HttpBridge* BuildBridge() {
+ if (!request_context_) {
+ request_context_ = new HttpBridge::RequestContext(
+ new TestURLRequestContext());
+ }
+ HttpBridge* bridge = new HttpBridge(request_context_,
+ io_thread_.message_loop());
+ bridge->use_io_loop_for_testing_ = true;
+ return bridge;
+ }
+
+ MessageLoop* io_thread_loop() { return io_thread_.message_loop(); }
+ private:
+ // Separate thread for IO used by the HttpBridge.
+ scoped_refptr<HttpBridge::RequestContext> request_context_;
+ base::Thread io_thread_;
+};
+
+// An HttpBridge that doesn't actually make network requests and just calls
+// back with dummy response info.
+class ShuntedHttpBridge : public HttpBridge {
+ public:
+ ShuntedHttpBridge(const URLRequestContext* baseline_context,
+ MessageLoop* io_loop, HttpBridgeTest* test)
+ : HttpBridge(new HttpBridge::RequestContext(baseline_context),
+ io_loop), test_(test) { }
+ protected:
+ virtual void MakeAsynchronousPost() {
+ ASSERT_TRUE(MessageLoop::current() == test_->io_thread_loop());
+ // We don't actually want to make a request for this test, so just callback
+ // as if it completed.
+ test_->io_thread_loop()->PostTask(FROM_HERE,
+ NewRunnableMethod(this, &ShuntedHttpBridge::CallOnURLFetchComplete));
+ }
+ private:
+ void CallOnURLFetchComplete() {
+ ASSERT_TRUE(MessageLoop::current() == test_->io_thread_loop());
+ // We return one cookie and a dummy content response.
+ ResponseCookies cookies;
+ cookies.push_back("cookie1");
+ std::string response_content = "success!";
+ OnURLFetchComplete(NULL, GURL("www.google.com"), URLRequestStatus(),
+ 200, cookies, response_content);
+ }
+ HttpBridgeTest* test_;
+};
+
+// Test the HttpBridge without actually making any network requests.
+TEST_F(HttpBridgeTest, TestMakeSynchronousPostShunted) {
+ scoped_refptr<HttpBridge> http_bridge(new ShuntedHttpBridge(
+ new TestURLRequestContext(), io_thread_loop(), this));
+ http_bridge->SetUserAgent("bob");
+ http_bridge->SetURL("http://www.google.com", 9999);
+ http_bridge->SetPostPayload("text/plain", 2, " ");
+
+ int os_error = 0;
+ int response_code = 0;
+ bool success = http_bridge->MakeSynchronousPost(&os_error, &response_code);
+ EXPECT_TRUE(success);
+ EXPECT_EQ(200, response_code);
+ EXPECT_EQ(0, os_error);
+ EXPECT_EQ(1, http_bridge->GetResponseCookieCount());
+ // TODO(timsteele): This is a valid test condition, it's just temporarily
+ // broken so that HttpBridge satisfies the ServerConnectionManager.
+#if FIXED_SYNC_BACKEND_COOKIE_PARSING
+ EXPECT_EQ(std::string("cookie1"),
+ std::string(http_bridge->GetResponseCookieAt(0)));
+#endif
+ EXPECT_EQ(8, http_bridge->GetResponseContentLength());
+ EXPECT_EQ(std::string("success!"),
+ std::string(http_bridge->GetResponseContent()));
+}
+
+// Full round-trip test of the HttpBridge, using default UA string and
+// no request cookies.
+TEST_F(HttpBridgeTest, TestMakeSynchronousPostLiveWithPayload) {
+ scoped_refptr<HTTPTestServer> server = HTTPTestServer::CreateServer(kDocRoot,
+ NULL);
+ ASSERT_TRUE(NULL != server.get());
+
+ scoped_refptr<HttpBridge> http_bridge(BuildBridge());
+
+ std::string payload = "this should be echoed back";
+ GURL echo = server->TestServerPage("echo");
+ http_bridge->SetURL(echo.spec().c_str(), echo.IntPort());
+ http_bridge->SetPostPayload("application/x-www-form-urlencoded",
+ payload.length() + 1, payload.c_str());
+ int os_error = 0;
+ int response_code = 0;
+ bool success = http_bridge->MakeSynchronousPost(&os_error, &response_code);
+ EXPECT_TRUE(success);
+ EXPECT_EQ(200, response_code);
+ EXPECT_EQ(0, os_error);
+ EXPECT_EQ(0, http_bridge->GetResponseCookieCount());
+ EXPECT_EQ(payload.length() + 1, http_bridge->GetResponseContentLength());
+ EXPECT_EQ(payload, std::string(http_bridge->GetResponseContent()));
+}
+
+// Full round-trip test of the HttpBridge, using custom UA string and
+// multiple request cookies.
+TEST_F(HttpBridgeTest, TestMakeSynchronousPostLiveComprehensive) {
+ scoped_refptr<HTTPTestServer> server = HTTPTestServer::CreateServer(kDocRoot,
+ NULL);
+ ASSERT_TRUE(NULL != server.get());
+ scoped_refptr<HttpBridge> http_bridge(BuildBridge());
+
+ GURL echo_header = server->TestServerPage("echoall");
+ http_bridge->SetUserAgent("bob");
+ http_bridge->SetURL(echo_header.spec().c_str(), echo_header.IntPort());
+ http_bridge->AddCookieForRequest("foo=bar");
+ http_bridge->AddCookieForRequest("baz=boo");
+ std::string test_payload = "###TEST PAYLOAD###";
+ http_bridge->SetPostPayload("text/html", test_payload.length() + 1,
+ test_payload.c_str());
+
+ int os_error = 0;
+ int response_code = 0;
+ bool success = http_bridge->MakeSynchronousPost(&os_error, &response_code);
+ EXPECT_TRUE(success);
+ EXPECT_EQ(200, response_code);
+ EXPECT_EQ(0, os_error);
+ EXPECT_EQ(0, http_bridge->GetResponseCookieCount());
+ std::string response = http_bridge->GetResponseContent();
+// TODO(timsteele): This is a valid test condition, it's just temporarily
+// broken so that HttpBridge satisfies the ServerConnectionManager; the format
+// seems to be surprising the TestServer, because it isn't echoing the headers
+// properly.
+#if FIXED_SYNCER_BACKEND_COOKIE_PARSING
+ EXPECT_NE(std::string::npos, response.find("Cookie: foo=bar; baz=boo"));
+ EXPECT_NE(std::string::npos, response.find("User-Agent: bob"));
+#endif
+ EXPECT_NE(std::string::npos, response.find(test_payload.c_str()));
+}
+
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/sync/glue/model_associator.cc b/chrome/browser/sync/glue/model_associator.cc
new file mode 100644
index 0000000..d4c4c45
--- /dev/null
+++ b/chrome/browser/sync/glue/model_associator.cc
@@ -0,0 +1,504 @@
+// Copyright (c) 2006-2009 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#include "chrome/browser/sync/glue/model_associator.h"
+
+#include <stack>
+
+#include "base/message_loop.h"
+#include "base/task.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/sync/engine/syncapi.h"
+#include "chrome/browser/sync/profile_sync_service.h"
+
+namespace browser_sync {
+
+// The sync protocol identifies top-level entities by means of well-known tags,
+// which should not be confused with titles. Each tag corresponds to a
+// singleton instance of a particular top-level node in a user's share; the
+// tags are consistent across users. The tags allow us to locate the specific
+// folders whose contents we care about synchronizing, without having to do a
+// lookup by name or path. The tags should not be made user-visible.
+// For example, the tag "bookmark_bar" represents the permanent node for
+// bookmarks bar in Chrome. The tag "other_bookmarks" represents the permanent
+// folder Other Bookmarks in Chrome.
+//
+// It is the responsibility of something upstream (at time of writing,
+// the sync server) to create these tagged nodes when initializing sync
+// for the first time for a user. Thus, once the backend finishes
+// initializing, the ProfileSyncService can rely on the presence of tagged
+// nodes.
+//
+// TODO(ncarter): Pull these tags from an external protocol specification
+// rather than hardcoding them here.
+static const wchar_t* kOtherBookmarksTag = L"other_bookmarks";
+static const wchar_t* kBookmarkBarTag = L"bookmark_bar";
+
+// Bookmark comparer for map of bookmark nodes.
+class BookmarkComparer {
+ public:
+ // Compares the two given nodes and returns whether node1 should appear
+ // before node2 in strict weak ordering.
+ bool operator()(const BookmarkNode* node1,
+ const BookmarkNode* node2) const {
+ DCHECK(node1);
+ DCHECK(node2);
+
+ // Keep folder nodes before non-folder nodes.
+ if (node1->is_folder() != node2->is_folder())
+ return node1->is_folder();
+
+ int result = node1->GetTitle().compare(node2->GetTitle());
+ if (result != 0)
+ return result < 0;
+
+ result = node1->GetURL().spec().compare(node2->GetURL().spec());
+ if (result != 0)
+ return result < 0;
+
+ return false;
+ }
+};
+
+// Provides the following abstraction: given a parent bookmark node, find best
+// matching child node for many sync nodes.
+class BookmarkNodeFinder {
+ public:
+ // Creats an instance with the given parent bookmark node.
+ explicit BookmarkNodeFinder(const BookmarkNode* parent_node);
+
+ // Finds best matching node for the given sync node.
+ // Returns the matching node if one exists; NULL otherwise. If a matching
+ // node is found, it's removed for further matches.
+ const BookmarkNode* FindBookmarkNode(const sync_api::BaseNode& sync_node);
+
+ private:
+ typedef std::set<const BookmarkNode*, BookmarkComparer> BookmarkNodesSet;
+
+ const BookmarkNode* parent_node_;
+ BookmarkNodesSet child_nodes_;
+
+ DISALLOW_COPY_AND_ASSIGN(BookmarkNodeFinder);
+};
+
+BookmarkNodeFinder::BookmarkNodeFinder(const BookmarkNode* parent_node)
+ : parent_node_(parent_node) {
+ for (int i = 0; i < parent_node_->GetChildCount(); ++i)
+ child_nodes_.insert(parent_node_->GetChild(i));
+}
+
+const BookmarkNode* BookmarkNodeFinder::FindBookmarkNode(
+ const sync_api::BaseNode& sync_node) {
+ // Create a bookmark node from the given sync node.
+ BookmarkNode temp_node(GURL(sync_node.GetURL()));
+ temp_node.SetTitle(UTF16ToWide(sync_node.GetTitle()));
+ if (sync_node.GetIsFolder())
+ temp_node.SetType(BookmarkNode::FOLDER);
+ else
+ temp_node.SetType(BookmarkNode::URL);
+
+ const BookmarkNode* result = NULL;
+ BookmarkNodesSet::iterator iter = child_nodes_.find(&temp_node);
+ if (iter != child_nodes_.end()) {
+ result = *iter;
+ // Remove the matched node so we don't match with it again.
+ child_nodes_.erase(iter);
+ }
+
+ return result;
+}
+
+ModelAssociator::ModelAssociator(ProfileSyncService* sync_service)
+ : sync_service_(sync_service),
+ task_pending_(false) {
+ DCHECK(sync_service_);
+}
+
+void ModelAssociator::ClearAll() {
+ id_map_.clear();
+ id_map_inverse_.clear();
+ dirty_assocations_sync_ids_.clear();
+}
+
+int64 ModelAssociator::GetSyncIdFromBookmarkId(int64 node_id) const {
+ BookmarkIdToSyncIdMap::const_iterator iter = id_map_.find(node_id);
+ return iter == id_map_.end() ? sync_api::kInvalidId : iter->second;
+}
+
+bool ModelAssociator::GetBookmarkIdFromSyncId(int64 sync_id,
+ int64* node_id) const {
+ SyncIdToBookmarkIdMap::const_iterator iter = id_map_inverse_.find(sync_id);
+ if (iter == id_map_inverse_.end())
+ return false;
+ *node_id = iter->second;
+ return true;
+}
+
+bool ModelAssociator::InitSyncNodeFromBookmarkId(
+ int64 node_id,
+ sync_api::BaseNode* sync_node) {
+ DCHECK(sync_node);
+ int64 sync_id = GetSyncIdFromBookmarkId(node_id);
+ if (sync_id == sync_api::kInvalidId)
+ return false;
+ if (!sync_node->InitByIdLookup(sync_id))
+ return false;
+ DCHECK(sync_node->GetId() == sync_id);
+ return true;
+}
+
+const BookmarkNode* ModelAssociator::GetBookmarkNodeFromSyncId(int64 sync_id) {
+ int64 node_id;
+ if (!GetBookmarkIdFromSyncId(sync_id, &node_id))
+ return false;
+ BookmarkModel* model = sync_service_->profile()->GetBookmarkModel();
+ return model->GetNodeByID(node_id);
+}
+
+void ModelAssociator::AssociateIds(int64 node_id, int64 sync_id) {
+ DCHECK_NE(sync_id, sync_api::kInvalidId);
+ DCHECK(id_map_.find(node_id) == id_map_.end());
+ DCHECK(id_map_inverse_.find(sync_id) == id_map_inverse_.end());
+ id_map_[node_id] = sync_id;
+ id_map_inverse_[sync_id] = node_id;
+ dirty_assocations_sync_ids_.insert(sync_id);
+ PostPersistAssociationsTask();
+}
+
+void ModelAssociator::DisassociateIds(int64 sync_id) {
+ SyncIdToBookmarkIdMap::iterator iter = id_map_inverse_.find(sync_id);
+ if (iter == id_map_inverse_.end())
+ return;
+ id_map_.erase(iter->second);
+ id_map_inverse_.erase(iter);
+ dirty_assocations_sync_ids_.erase(sync_id);
+}
+
+bool ModelAssociator::BookmarkModelHasUserCreatedNodes() const {
+ BookmarkModel* model = sync_service_->profile()->GetBookmarkModel();
+ DCHECK(model->IsLoaded());
+ return model->GetBookmarkBarNode()->GetChildCount() > 0 ||
+ model->other_node()->GetChildCount() > 0;
+}
+
+bool ModelAssociator::SyncModelHasUserCreatedNodes() {
+ int64 bookmark_bar_sync_id;
+ if (!GetSyncIdForTaggedNode(WideToUTF16(kBookmarkBarTag),
+ &bookmark_bar_sync_id)) {
+ NOTREACHED();
+ return false;
+ }
+ int64 other_bookmarks_sync_id;
+ if (!GetSyncIdForTaggedNode(WideToUTF16(kOtherBookmarksTag),
+ &other_bookmarks_sync_id)) {
+ NOTREACHED();
+ return false;
+ }
+
+ sync_api::ReadTransaction trans(
+ sync_service_->backend()->GetUserShareHandle());
+
+ sync_api::ReadNode bookmark_bar_node(&trans);
+ if (!bookmark_bar_node.InitByIdLookup(bookmark_bar_sync_id)) {
+ NOTREACHED();
+ return false;
+ }
+
+ sync_api::ReadNode other_bookmarks_node(&trans);
+ if (!other_bookmarks_node.InitByIdLookup(other_bookmarks_sync_id)) {
+ NOTREACHED();
+ return false;
+ }
+
+ // Sync model has user created nodes if either one of the permanent nodes
+ // has children.
+ return bookmark_bar_node.GetFirstChildId() != sync_api::kInvalidId ||
+ other_bookmarks_node.GetFirstChildId() != sync_api::kInvalidId;
+}
+
+bool ModelAssociator::NodesMatch(const BookmarkNode* bookmark,
+ const sync_api::BaseNode* sync_node) const {
+ if (bookmark->GetTitle() != UTF16ToWide(sync_node->GetTitle()))
+ return false;
+ if (bookmark->is_folder() != sync_node->GetIsFolder())
+ return false;
+ if (bookmark->is_url()) {
+ if (bookmark->GetURL() != GURL(sync_node->GetURL()))
+ return false;
+ }
+ // Don't compare favicons here, because they are not really
+ // user-updated and we don't have versioning information -- a site changing
+ // its favicon shouldn't result in a bookmark mismatch.
+ return true;
+}
+
+bool ModelAssociator::AssociateTaggedPermanentNode(
+ const BookmarkNode* permanent_node,
+ const string16 &tag) {
+ // Do nothing if |permanent_node| is already initialized and associated.
+ int64 sync_id = GetSyncIdFromBookmarkId(permanent_node->id());
+ if (sync_id != sync_api::kInvalidId)
+ return true;
+ if (!GetSyncIdForTaggedNode(tag, &sync_id))
+ return false;
+
+ AssociateIds(permanent_node->id(), sync_id);
+ return true;
+}
+
+bool ModelAssociator::GetSyncIdForTaggedNode(const string16& tag,
+ int64* sync_id) {
+ sync_api::ReadTransaction trans(
+ sync_service_->backend()->GetUserShareHandle());
+ sync_api::ReadNode sync_node(&trans);
+ if (!sync_node.InitByTagLookup(tag.c_str()))
+ return false;
+ *sync_id = sync_node.GetId();
+ return true;
+}
+
+bool ModelAssociator::AssociateModels() {
+ // Try to load model associations from persisted associations first. If that
+ // succeeds, we don't need to run the complex model matching algorithm.
+ if (LoadAssociations())
+ return true;
+
+ ClearAll();
+
+ // We couldn't load model assocations from persisted assocations. So build
+ // them.
+ return BuildAssocations();
+}
+
+bool ModelAssociator::BuildAssocations() {
+ // Algorithm description:
+ // Match up the roots and recursively do the following:
+ // * For each sync node for the current sync parent node, find the best
+ // matching bookmark node under the corresponding bookmark parent node.
+ // If no matching node is found, create a new bookmark node in the same
+ // position as the corresponding sync node.
+ // If a matching node is found, update the properties of it from the
+ // corresponding sync node.
+ // * When all children sync nodes are done, add the extra children bookmark
+ // nodes to the sync parent node.
+ //
+ // This algorithm will do a good job of merging when folder names are a good
+ // indicator of the two folders being the same. It will handle reordering and
+ // new node addition very well (without creating duplicates).
+ // This algorithm will not do well if the folder name has changes but the
+ // children under them are all the same.
+
+ BookmarkModel* model = sync_service_->profile()->GetBookmarkModel();
+ DCHECK(model->IsLoaded());
+
+ // To prime our association, we associate the top-level nodes, Bookmark Bar
+ // and Other Bookmarks.
+ if (!AssociateTaggedPermanentNode(model->other_node(),
+ WideToUTF16(kOtherBookmarksTag))) {
+ NOTREACHED() << "Server did not create top-level nodes. Possibly we "
+ << "are running against an out-of-date server?";
+ return false;
+ }
+ if (!AssociateTaggedPermanentNode(model->GetBookmarkBarNode(),
+ WideToUTF16(kBookmarkBarTag))) {
+ NOTREACHED() << "Server did not create top-level nodes. Possibly we "
+ << "are running against an out-of-date server?";
+ return false;
+ }
+ int64 bookmark_bar_sync_id = GetSyncIdFromBookmarkId(
+ model->GetBookmarkBarNode()->id());
+ DCHECK(bookmark_bar_sync_id != sync_api::kInvalidId);
+ int64 other_bookmarks_sync_id = GetSyncIdFromBookmarkId(
+ model->other_node()->id());
+ DCHECK(other_bookmarks_sync_id!= sync_api::kInvalidId);
+
+ std::stack<int64> dfs_stack;
+ dfs_stack.push(other_bookmarks_sync_id);
+ dfs_stack.push(bookmark_bar_sync_id);
+
+ sync_api::WriteTransaction trans(
+ sync_service_->backend()->GetUserShareHandle());
+
+ while (!dfs_stack.empty()) {
+ int64 sync_parent_id = dfs_stack.top();
+ dfs_stack.pop();
+
+ sync_api::ReadNode sync_parent(&trans);
+ if (!sync_parent.InitByIdLookup(sync_parent_id)) {
+ NOTREACHED();
+ return false;
+ }
+ // Only folder nodes are pushed on to the stack.
+ DCHECK(sync_parent.GetIsFolder());
+
+ const BookmarkNode* parent_node = GetBookmarkNodeFromSyncId(sync_parent_id);
+ DCHECK(parent_node->is_folder());
+
+ BookmarkNodeFinder node_finder(parent_node);
+
+ int index = 0;
+ int64 sync_child_id = sync_parent.GetFirstChildId();
+ while (sync_child_id != sync_api::kInvalidId) {
+ sync_api::WriteNode sync_child_node(&trans);
+ if (!sync_child_node.InitByIdLookup(sync_child_id)) {
+ NOTREACHED();
+ return false;
+ }
+
+ const BookmarkNode* child_node = NULL;
+ child_node = node_finder.FindBookmarkNode(sync_child_node);
+ if (child_node) {
+ model->Move(child_node, parent_node, index);
+ // Set the favicon for bookmark node from sync node or vice versa.
+ if (!sync_service_->SetBookmarkFavicon(&sync_child_node, child_node))
+ sync_service_->SetSyncNodeFavicon(child_node, &sync_child_node);
+ } else {
+ // Create a new bookmark node for the sync node.
+ child_node = sync_service_->CreateBookmarkNode(&sync_child_node,
+ parent_node,
+ index);
+ }
+ AssociateIds(child_node->id(), sync_child_id);
+ if (sync_child_node.GetIsFolder())
+ dfs_stack.push(sync_child_id);
+
+ sync_child_id = sync_child_node.GetSuccessorId();
+ ++index;
+ }
+
+ // At this point all the children nodes of the parent sync node have
+ // corresponding children in the parent bookmark node and they are all in
+ // the right positions: from 0 to index - 1.
+ // So the children starting from index in the parent bookmark node are the
+ // ones that are not present in the parent sync node. So create them.
+ for (int i = index; i < parent_node->GetChildCount(); ++i) {
+ sync_child_id = sync_service_->CreateSyncNode(parent_node, i, &trans);
+ if (parent_node->GetChild(i)->is_folder())
+ dfs_stack.push(sync_child_id);
+ }
+ }
+ return true;
+}
+
+void ModelAssociator::PostPersistAssociationsTask() {
+ // No need to post a task if a task is already pending.
+ if (task_pending_)
+ return;
+ task_pending_ = true;
+ MessageLoop::current()->PostTask(
+ FROM_HERE,
+ NewRunnableMethod(this, &ModelAssociator::PersistAssociations));
+}
+
+void ModelAssociator::PersistAssociations() {
+ DCHECK(task_pending_);
+ task_pending_ = false;
+
+ // If there are no dirty assocations we have nothing to do. We handle this
+ // explicity instead of letting the for loop do it to avoid creating a write
+ // transaction in this case.
+ if (dirty_assocations_sync_ids_.empty()) {
+ DCHECK(id_map_.empty());
+ DCHECK(id_map_inverse_.empty());
+ return;
+ }
+
+ sync_api::WriteTransaction trans(
+ sync_service_->backend()->GetUserShareHandle());
+ DirtyAssocationsSyncIds::iterator iter;
+ for (iter = dirty_assocations_sync_ids_.begin();
+ iter != dirty_assocations_sync_ids_.end();
+ ++iter) {
+ int64 sync_id = *iter;
+ sync_api::WriteNode sync_node(&trans);
+ if (!sync_node.InitByIdLookup(sync_id)) {
+ sync_service_->SetUnrecoverableError();
+ return;
+ }
+ int64 node_id;
+ if (GetBookmarkIdFromSyncId(sync_id, &node_id))
+ sync_node.SetExternalId(node_id);
+ else
+ NOTREACHED();
+ }
+ dirty_assocations_sync_ids_.clear();
+}
+
+bool ModelAssociator::LoadAssociations() {
+ BookmarkModel* model = sync_service_->profile()->GetBookmarkModel();
+ DCHECK(model->IsLoaded());
+ // If the bookmarks changed externally, our previous assocations may not be
+ // valid; so return false.
+ if (model->file_changed())
+ return false;
+
+ // Our persisted assocations should be valid. Try to populate id assocation
+ // maps using persisted assocations.
+
+ int64 other_bookmarks_id;
+ if (!GetSyncIdForTaggedNode(WideToUTF16(kOtherBookmarksTag),
+ &other_bookmarks_id)) {
+ NOTREACHED(); // We should always be able to find the permanent nodes.
+ return false;
+ }
+ int64 bookmark_bar_id;
+ if (!GetSyncIdForTaggedNode(WideToUTF16(kBookmarkBarTag), &bookmark_bar_id)) {
+ NOTREACHED(); // We should always be able to find the permanent nodes.
+ return false;
+ }
+
+ std::stack<int64> dfs_stack;
+ dfs_stack.push(other_bookmarks_id);
+ dfs_stack.push(bookmark_bar_id);
+
+ sync_api::ReadTransaction trans(
+ sync_service_->backend()->GetUserShareHandle());
+
+ while (!dfs_stack.empty()) {
+ int64 parent_id = dfs_stack.top();
+ dfs_stack.pop();
+ sync_api::ReadNode sync_parent(&trans);
+ if (!sync_parent.InitByIdLookup(parent_id)) {
+ NOTREACHED();
+ return false;
+ }
+
+ int64 external_id = sync_parent.GetExternalId();
+ if (external_id == 0)
+ return false;
+
+ const BookmarkNode* node = model->GetNodeByID(external_id);
+ if (!node)
+ return false;
+
+ // Don't try to call NodesMatch on permanent nodes like bookmark bar and
+ // other bookmarks. They are not expected to match.
+ if (node != model->GetBookmarkBarNode() &&
+ node != model->other_node() &&
+ !NodesMatch(node, &sync_parent))
+ return false;
+
+ AssociateIds(external_id, sync_parent.GetId());
+
+ // Add all children of the current node to the stack.
+ int64 child_id = sync_parent.GetFirstChildId();
+ while (child_id != sync_api::kInvalidId) {
+ dfs_stack.push(child_id);
+ sync_api::ReadNode child_node(&trans);
+ if (!child_node.InitByIdLookup(child_id)) {
+ NOTREACHED();
+ return false;
+ }
+ child_id = child_node.GetSuccessorId();
+ }
+ }
+ DCHECK(dfs_stack.empty());
+ return true;
+}
+
+} // namespace browser_sync
+
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/sync/glue/model_associator.h b/chrome/browser/sync/glue/model_associator.h
new file mode 100644
index 0000000..9d5f825
--- /dev/null
+++ b/chrome/browser/sync/glue/model_associator.h
@@ -0,0 +1,141 @@
+// Copyright (c) 2006-2009 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#ifndef CHROME_BROWSER_SYNC_GLUE_MODEL_ASSOCATOR_H_
+#define CHROME_BROWSER_SYNC_GLUE_MODEL_ASSOCATOR_H_
+
+#include <map>
+#include <set>
+#include <string>
+
+#include "base/basictypes.h"
+#include "base/ref_counted.h"
+#include "base/scoped_ptr.h"
+#include "base/string16.h"
+
+class BookmarkNode;
+
+namespace sync_api {
+class BaseNode;
+class BaseTransaction;
+class ReadNode;
+}
+
+class ProfileSyncService;
+
+namespace browser_sync {
+
+// Contains all model assocation related logic:
+// * Algorithm to associate bookmark model and sync model.
+// * Methods to get a bookmark node for a given sync node and vice versa.
+// * Persisting model assocations and loading them back.
+class ModelAssociator
+ : public base::RefCountedThreadSafe<ModelAssociator> {
+ public:
+ explicit ModelAssociator(ProfileSyncService* sync_service);
+ virtual ~ModelAssociator() { }
+
+ // Clears all assocations.
+ void ClearAll();
+
+ // Returns sync id for the given bookmark node id.
+ // Returns sync_api::kInvalidId if the sync node is not found for the given
+ // bookmark node id.
+ int64 GetSyncIdFromBookmarkId(int64 node_id) const;
+
+ // Stores bookmark node id for the given sync id in bookmark_id. Returns true
+ // if the bookmark id was successfully found; false otherwise.
+ bool GetBookmarkIdFromSyncId(int64 sync_id, int64* bookmark_id) const;
+
+ // Initializes the given sync node from the given bookmark node id.
+ // Returns false if no sync node was found for the given bookmark node id or
+ // if the initialization of sync node fails.
+ bool InitSyncNodeFromBookmarkId(int64 node_id, sync_api::BaseNode* sync_node);
+
+ // Returns the bookmark node for the given sync id.
+ // Returns NULL if no bookmark node is found for the given sync id.
+ const BookmarkNode* GetBookmarkNodeFromSyncId(int64 sync_id);
+
+ // Associates the given bookmark node id with the given sync id.
+ void AssociateIds(int64 node_id, int64 sync_id);
+ // Disassociate the ids that correspond to the given sync id.
+ void DisassociateIds(int64 sync_id);
+
+ // Returns whether the bookmark model has user created nodes or not. That is,
+ // whether there are nodes in the bookmark model except the bookmark bar and
+ // other bookmarks.
+ bool BookmarkModelHasUserCreatedNodes() const;
+
+ // Returns whether the sync model has nodes other than the permanent tagged
+ // nodes.
+ bool SyncModelHasUserCreatedNodes();
+
+ // AssociateModels iterates through both the sync and the browser
+ // bookmark model, looking for matched pairs of items. For any pairs it
+ // finds, it will call AssociateSyncID. For any unmatched items,
+ // MergeAndAssociateModels will try to repair the match, e.g. by adding a new
+ // node. After successful completion, the models should be identical and
+ // corresponding. Returns true on success. On failure of this step, we
+ // should abort the sync operation and report an error to the user.
+ bool AssociateModels();
+
+ protected:
+ // Stores the id of the node with the given tag in |sync_id|.
+ // Returns of that node was found successfully.
+ // Tests override this.
+ virtual bool GetSyncIdForTaggedNode(const string16& tag, int64* sync_id);
+
+ // Returns sync service instance.
+ ProfileSyncService* sync_service() { return sync_service_; }
+
+ private:
+ typedef std::map<int64, int64> BookmarkIdToSyncIdMap;
+ typedef std::map<int64, int64> SyncIdToBookmarkIdMap;
+ typedef std::set<int64> DirtyAssocationsSyncIds;
+
+ // Posts a task to persist dirty assocations.
+ void PostPersistAssociationsTask();
+ // Persists all dirty assocations.
+ void PersistAssociations();
+
+ // Loads the persisted assocations into in-memory maps.
+ // If the persisted associations are out-of-date due to some reason, returns
+ // false; otehrwise returns true.
+ bool LoadAssociations();
+
+ // Matches up the bookmark model and the sync model to build model
+ // assocations.
+ bool BuildAssocations();
+
+ // Associate a top-level node of the bookmark model with a permanent node in
+ // the sync domain. Such permanent nodes are identified by a tag that is
+ // well known to the server and the client, and is unique within a particular
+ // user's share. For example, "other_bookmarks" is the tag for the Other
+ // Bookmarks folder. The sync nodes are server-created.
+ bool AssociateTaggedPermanentNode(const BookmarkNode* permanent_node,
+ const string16& tag);
+
+ // Compare the properties of a pair of nodes from either domain.
+ bool NodesMatch(const BookmarkNode* bookmark,
+ const sync_api::BaseNode* sync_node) const;
+
+ ProfileSyncService* sync_service_;
+ BookmarkIdToSyncIdMap id_map_;
+ SyncIdToBookmarkIdMap id_map_inverse_;
+ // Stores sync ids for dirty associations.
+ DirtyAssocationsSyncIds dirty_assocations_sync_ids_;
+
+ // Indicates whether there is already a pending task to persist dirty model
+ // associations.
+ bool task_pending_;
+
+ DISALLOW_COPY_AND_ASSIGN(ModelAssociator);
+};
+
+} // namespace browser_sync
+
+#endif // CHROME_BROWSER_SYNC_GLUE_MODEL_ASSOCATOR_H_
+#endif // CHROME_PERSONALIZATION \ No newline at end of file
diff --git a/chrome/browser/sync/glue/sync_backend_host.cc b/chrome/browser/sync/glue/sync_backend_host.cc
new file mode 100644
index 0000000..ee75a79
--- /dev/null
+++ b/chrome/browser/sync/glue/sync_backend_host.cc
@@ -0,0 +1,308 @@
+// Copyright (c) 2006-2008 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#include "base/file_version_info.h"
+#include "base/file_util.h"
+#include "base/string_util.h"
+#include "chrome/browser/sync/glue/sync_backend_host.h"
+#include "chrome/browser/sync/glue/http_bridge.h"
+#include "chrome/browser/sync/glue/bookmark_model_worker.h"
+#include "webkit/glue/webkit_glue.h"
+
+static const char kSwitchSyncServiceURL[] = "sync-url";
+static const char kSwitchSyncServicePort[] = "sync-port";
+static const int kSaveChangesIntervalSeconds = 10;
+static const char kGaiaServiceId[] = "chromiumsync";
+static const char kGaiaSourceForChrome[] = "ChromiumBrowser";
+static const FilePath::CharType kSyncDataFolderName[] =
+ FILE_PATH_LITERAL("Sync Data");
+
+namespace browser_sync {
+
+SyncBackendHost::SyncBackendHost(SyncFrontend* frontend,
+ const FilePath& profile_path)
+ : core_thread_("Chrome_SyncCoreThread"),
+ frontend_loop_(MessageLoop::current()),
+ bookmark_model_worker_(NULL),
+ frontend_(frontend),
+ sync_data_folder_path_(profile_path.Append(kSyncDataFolderName)),
+ last_auth_error_(AUTH_ERROR_NONE) {
+ core_ = new Core(this);
+}
+
+SyncBackendHost::~SyncBackendHost() {
+ DCHECK(!core_ && !frontend_) << "Must call Shutdown before destructor.";
+}
+
+void SyncBackendHost::Initialize(const GURL& sync_service_url) {
+ if (!core_thread_.Start())
+ return;
+
+ bookmark_model_worker_ = new BookmarkModelWorker(frontend_loop_);
+ core_thread_.message_loop()->PostTask(FROM_HERE,
+ NewRunnableMethod(core_.get(), &SyncBackendHost::Core::DoInitialize,
+ sync_service_url, bookmark_model_worker_, true));
+}
+
+void SyncBackendHost::Authenticate(const std::string& username,
+ const std::string& password) {
+ core_thread_.message_loop()->PostTask(FROM_HERE,
+ NewRunnableMethod(core_.get(), &SyncBackendHost::Core::DoAuthenticate,
+ username, password));
+}
+
+void SyncBackendHost::Shutdown(bool sync_disabled) {
+ // Thread shutdown should occur in the following order:
+ // - SyncerThread
+ // - CoreThread
+ // - UI Thread (stops some time after we return from this call).
+ core_thread_.message_loop()->PostTask(FROM_HERE,
+ NewRunnableMethod(core_.get(),
+ &SyncBackendHost::Core::DoShutdown,
+ sync_disabled));
+
+ // Before joining the core_thread_, we wait for the BookmarkModelWorker to
+ // give us the green light that it is not depending on the frontend_loop_ to
+ // process any more tasks. Stop() blocks until this termination condition
+ // is true.
+ bookmark_model_worker_->Stop();
+
+ // Stop will return once the thread exits, which will be after DoShutdown
+ // runs. DoShutdown needs to run from core_thread_ because the sync backend
+ // requires any thread that opened sqlite handles to relinquish them
+ // personally. We need to join threads, because otherwise the main Chrome
+ // thread (ui loop) can exit before DoShutdown finishes, at which point
+ // virtually anything the sync backend does (or the post-back to
+ // frontend_loop_ by our Core) will epically fail because the CRT won't be
+ // initialized. For now this only ever happens at sync-enabled-Chrome exit,
+ // meaning bug 1482548 applies to prolonged "waiting" that may occur in
+ // DoShutdown.
+ core_thread_.Stop();
+
+ bookmark_model_worker_ = NULL;
+ frontend_ = NULL;
+ core_ = NULL; // Releases reference to core_.
+}
+
+void SyncBackendHost::Core::NotifyFrontend(FrontendNotification notification) {
+ if (!host_ || !host_->frontend_) {
+ return; // This can happen in testing because the UI loop processes tasks
+ // after an instance of SyncBackendHost was destroyed. In real
+ // life this doesn't happen.
+ }
+ switch (notification) {
+ case INITIALIZED:
+ host_->frontend_->OnBackendInitialized();
+ return;
+ case SYNC_CYCLE_COMPLETED:
+ host_->frontend_->OnSyncCycleCompleted();
+ return;
+ }
+}
+
+SyncBackendHost::UserShareHandle SyncBackendHost::GetUserShareHandle() const {
+ return core_->syncapi()->GetUserShare();
+}
+
+SyncBackendHost::Status SyncBackendHost::GetDetailedStatus() {
+ return core_->syncapi()->GetDetailedStatus();
+}
+
+SyncBackendHost::StatusSummary SyncBackendHost::GetStatusSummary() {
+ return core_->syncapi()->GetStatusSummary();
+}
+
+string16 SyncBackendHost::GetAuthenticatedUsername() const {
+ return UTF8ToUTF16(core_->syncapi()->GetAuthenticatedUsername());
+}
+
+AuthErrorState SyncBackendHost::GetAuthErrorState() const {
+ return last_auth_error_;
+}
+
+SyncBackendHost::Core::Core(SyncBackendHost* backend)
+ : host_(backend),
+ syncapi_(new sync_api::SyncManager()) {
+}
+
+// Helper to construct a user agent string (ASCII) suitable for use by
+// the syncapi for any HTTP communication. This string is used by the sync
+// backend for classifying client types when calculating statistics.
+std::string MakeUserAgentForSyncapi() {
+ std::string user_agent;
+ user_agent = "Chrome ";
+#if defined(OS_WIN)
+ user_agent += "WIN ";
+#elif defined(OS_LINUX)
+ user_agent += "LINUX ";
+#elif defined(OS_MACOSX)
+ user_agent += "MAC ";
+#endif
+ scoped_ptr<FileVersionInfo> version_info(
+ FileVersionInfo::CreateFileVersionInfoForCurrentModule());
+ if (version_info == NULL) {
+ DLOG(ERROR) << "Unable to create FileVersionInfo object";
+ return user_agent;
+ }
+
+ user_agent += WideToASCII(version_info->product_version());
+ user_agent += " (" + WideToASCII(version_info->last_change()) + ")";
+ if (!version_info->is_official_build())
+ user_agent += "-devel";
+ return user_agent;
+}
+
+void SyncBackendHost::Core::DoInitialize(
+ const GURL& service_url,
+ BookmarkModelWorker* bookmark_model_worker,
+ bool attempt_last_user_authentication) {
+ DCHECK(MessageLoop::current() == host_->core_thread_.message_loop());
+
+ // Make sure that the directory exists before initializing the backend.
+ // If it already exists, this will do no harm.
+ bool success = file_util::CreateDirectory(host_->sync_data_folder_path());
+ DCHECK(success);
+
+ syncapi_->SetObserver(this);
+ string16 path_str;
+#if defined (OS_WIN)
+ path_str = host_->sync_data_folder_path().value();
+#elif (defined(OS_LINUX) || defined(OS_MACOSX))
+ path_str = UTF8ToUTF16(sync_data_folder_path().value());
+#endif
+ success = syncapi_->Init(path_str.c_str(),
+ (service_url.host() + service_url.path()).c_str(),
+ service_url.EffectiveIntPort(),
+ kGaiaServiceId,
+ kGaiaSourceForChrome,
+ service_url.SchemeIsSecure(),
+ new HttpBridgeFactory(),
+ new HttpBridgeFactory(),
+ bookmark_model_worker,
+ attempt_last_user_authentication,
+ MakeUserAgentForSyncapi().c_str());
+ DCHECK(success) << "Syncapi initialization failed!";
+}
+
+void SyncBackendHost::Core::DoAuthenticate(const std::string& username,
+ const std::string& password) {
+ DCHECK(MessageLoop::current() == host_->core_thread_.message_loop());
+ syncapi_->Authenticate(username.c_str(), password.c_str());
+}
+
+void SyncBackendHost::Core::DoShutdown(bool sync_disabled) {
+ DCHECK(MessageLoop::current() == host_->core_thread_.message_loop());
+
+ save_changes_timer_.Stop();
+ syncapi_->Shutdown(); // Stops the SyncerThread.
+ syncapi_->RemoveObserver();
+ host_->bookmark_model_worker_->OnSyncerShutdownComplete();
+
+ if (sync_disabled &&
+ file_util::DirectoryExists(host_->sync_data_folder_path())) {
+ // Delete the sync data folder to cleanup backend data.
+ bool success = file_util::Delete(host_->sync_data_folder_path(), true);
+ DCHECK(success);
+ }
+
+ host_ = NULL;
+}
+
+static AuthErrorState AuthProblemToAuthError(
+ const sync_api::SyncManager::AuthProblem& auth_problem) {
+ switch (auth_problem) {
+ case sync_api::SyncManager::AUTH_PROBLEM_NONE:
+ return AUTH_ERROR_NONE;
+ case sync_api::SyncManager::AUTH_PROBLEM_INVALID_GAIA_CREDENTIALS:
+ return AUTH_ERROR_INVALID_GAIA_CREDENTIALS;
+ case sync_api::SyncManager::AUTH_PROBLEM_CONNECTION_FAILED:
+ return AUTH_ERROR_CONNECTION_FAILED;
+ case sync_api::SyncManager::AUTH_PROBLEM_USER_NOT_SIGNED_UP:
+ return AUTH_ERROR_USER_NOT_SIGNED_UP;
+ }
+
+ NOTREACHED() << "Unknown AuthProblem.";
+ return AUTH_ERROR_NONE;
+}
+
+void SyncBackendHost::Core::OnChangesApplied(
+ const sync_api::BaseTransaction* trans,
+ const sync_api::SyncManager::ChangeRecord* changes,
+ int change_count) {
+ if (!host_ || !host_->frontend_) {
+ DCHECK(false) << "OnChangesApplied called after Shutdown?";
+ return;
+ }
+
+ // ChangesApplied is the one exception that should come over from the sync
+ // backend already on the service_loop_ thanks to our BookmarkModelWorker.
+ // SyncFrontend changes exclusively on the UI loop, because it updates
+ // the bookmark model. As such, we don't need to worry about changes that
+ // have been made to the bookmark model but not yet applied to the sync
+ // model -- such changes only happen on the UI loop, and there's no
+ // contention.
+ if (host_->frontend_loop_ != MessageLoop::current()) {
+ // TODO(ncarter): Bug 1480644. Make this a DCHECK once syncapi filters
+ // out all irrelevant changes.
+ DLOG(WARNING) << "Could not update bookmark model from non-UI thread";
+ return;
+ }
+ host_->frontend_->ApplyModelChanges(trans, changes, change_count);
+}
+
+void SyncBackendHost::Core::OnSyncCycleCompleted() {
+ host_->frontend_loop_->PostTask(FROM_HERE, NewRunnableMethod(this,
+ &Core::NotifyFrontend, SYNC_CYCLE_COMPLETED));
+}
+
+void SyncBackendHost::Core::OnInitializationComplete() {
+ if (!host_ || !host_->frontend_)
+ return; // We may have been told to Shutdown before initialization
+ // completed.
+
+ // We could be on some random sync backend thread, so MessageLoop::current()
+ // can definitely be null in here.
+ host_->frontend_loop_->PostTask(FROM_HERE,
+ NewRunnableMethod(this, &Core::NotifyFrontend, INITIALIZED));
+
+ // Initialization is complete, so we can schedule recurring SaveChanges.
+ host_->core_thread_.message_loop()->PostTask(FROM_HERE,
+ NewRunnableMethod(this, &Core::StartSavingChanges));
+}
+
+void SyncBackendHost::Core::OnAuthProblem(
+ sync_api::SyncManager::AuthProblem auth_problem) {
+ // We could be on SyncEngine_AuthWatcherThread. Post to our core loop so
+ // we can modify state.
+ host_->frontend_loop_->PostTask(FROM_HERE,
+ NewRunnableMethod(this, &Core::HandleAuthErrorEventOnFrontendLoop,
+ AuthProblemToAuthError(auth_problem)));
+}
+
+void SyncBackendHost::Core::HandleAuthErrorEventOnFrontendLoop(
+ AuthErrorState new_auth_error) {
+ if (!host_ || !host_->frontend_)
+ return;
+
+ DCHECK_EQ(MessageLoop::current(), host_->frontend_loop_);
+
+ host_->last_auth_error_ = new_auth_error;
+ host_->frontend_->OnAuthError();
+}
+
+void SyncBackendHost::Core::StartSavingChanges() {
+ save_changes_timer_.Start(
+ base::TimeDelta::FromSeconds(kSaveChangesIntervalSeconds),
+ this, &Core::SaveChanges);
+}
+
+void SyncBackendHost::Core::SaveChanges() {
+ syncapi_->SaveChanges();
+}
+
+} // namespace browser_sync
+
+#endif
diff --git a/chrome/browser/sync/glue/sync_backend_host.h b/chrome/browser/sync/glue/sync_backend_host.h
new file mode 100644
index 0000000..eb89e43
--- /dev/null
+++ b/chrome/browser/sync/glue/sync_backend_host.h
@@ -0,0 +1,274 @@
+// Copyright (c) 2006-2008 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#ifndef CHROME_BROWSER_SYNC_GLUE_SYNC_BACKEND_HOST_H_
+#define CHROME_BROWSER_SYNC_GLUE_SYNC_BACKEND_HOST_H_
+
+#include <string>
+
+#include "base/file_path.h"
+#include "base/lock.h"
+#include "base/message_loop.h"
+#include "base/ref_counted.h"
+#include "base/thread.h"
+#include "base/timer.h"
+#include "chrome/browser/sync/auth_error_state.h"
+#include "chrome/browser/sync/engine/syncapi.h"
+#include "chrome/browser/sync/glue/bookmark_model_worker.h"
+#include "googleurl/src/gurl.h"
+
+namespace browser_sync {
+
+class SyncFrontend;
+
+// A UI-thread safe API into the sync backend that "hosts" the top-level
+// syncapi element, the SyncManager, on its own thread. This class handles
+// dispatch of potentially blocking calls to appropriate threads and ensures
+// that the SyncFrontend is only accessed on the UI loop.
+class SyncBackendHost {
+ public:
+ typedef sync_api::UserShare* UserShareHandle;
+ typedef sync_api::SyncManager::Status::Summary StatusSummary;
+ typedef sync_api::SyncManager::Status Status;
+
+ // Create a SyncBackendHost with a reference to the |frontend| that it serves
+ // and communicates to via the SyncFrontend interface (on the same thread
+ // it used to call the constructor).
+ SyncBackendHost(SyncFrontend* frontend, const FilePath& proifle_path);
+ ~SyncBackendHost();
+
+ // Called on |frontend_loop_| to kick off asynchronous initialization.
+ void Initialize(const GURL& service_url);
+
+ // Called on |frontend_loop_| to kick off asynchronous authentication.
+ void Authenticate(const std::string& username, const std::string& password);
+
+ // Called on |frontend_loop_| to kick off shutdown.
+ // |sync_disabled| indicates if syncing is being disabled or not.
+ // See the implementation and Core::DoShutdown for details.
+ void Shutdown(bool sync_disabled);
+
+ // Called on |frontend_loop_| to obtain a handle to the UserShare needed
+ // for creating transactions.
+ UserShareHandle GetUserShareHandle() const;
+
+ // Called from any thread to obtain current status information in detailed or
+ // summarized form.
+ Status GetDetailedStatus();
+ StatusSummary GetStatusSummary();
+ AuthErrorState GetAuthErrorState() const;
+
+ const FilePath& sync_data_folder_path() const {
+ return sync_data_folder_path_;
+ }
+
+ // Returns the authenticated username of the sync user, or empty if none
+ // exists. It will only exist if the authentication service provider (e.g
+ // GAIA) has confirmed the username is authentic.
+ string16 GetAuthenticatedUsername() const;
+
+#ifdef UNIT_TEST
+ // Called from unit test to bypass authentication and initialize the syncapi
+ // to a state suitable for testing but not production.
+ void InitializeForTestMode(const std::wstring& test_user) {
+ if (!core_thread_.Start())
+ return;
+ bookmark_model_worker_ = new BookmarkModelWorker(frontend_loop_);
+ core_thread_.message_loop()->PostTask(FROM_HERE,
+ NewRunnableMethod(core_.get(),
+ &SyncBackendHost::Core::DoInitializeForTest,
+ bookmark_model_worker_,
+ test_user));
+ }
+#endif
+
+ private:
+ // The real guts of SyncBackendHost, to keep the public client API clean.
+ class Core : public base::RefCountedThreadSafe<SyncBackendHost::Core>,
+ public sync_api::SyncManager::Observer {
+ public:
+ explicit Core(SyncBackendHost* backend);
+
+ // Note: This destructor should *always* be called from the thread that
+ // created it, and *always* after core_thread_ has exited. The syncapi
+ // watches thread exit events and keeps pointers to objects this dtor will
+ // destroy, so this ordering is important.
+ ~Core() {
+ }
+
+ // SyncManager::Observer implementation. The Core just acts like an air
+ // traffic controller here, forwarding incoming messages to appropriate
+ // landing threads.
+ virtual void OnChangesApplied(
+ const sync_api::BaseTransaction* trans,
+ const sync_api::SyncManager::ChangeRecord* changes,
+ int change_count);
+ virtual void OnSyncCycleCompleted();
+ virtual void OnInitializationComplete();
+ virtual void OnAuthProblem(
+ sync_api::SyncManager::AuthProblem auth_problem);
+
+ // Note:
+ //
+ // The Do* methods are the various entry points from our SyncBackendHost.
+ // It calls us on a dedicated thread to actually perform synchronous
+ // (and potentially blocking) syncapi operations.
+ //
+ // Called on the SyncBackendHost core_thread_ to perform initialization
+ // of the syncapi on behalf of SyncBackendHost::Initialize.
+ void DoInitialize(const GURL& service_url,
+ BookmarkModelWorker* bookmark_model_worker_,
+ bool attempt_last_user_authentication);
+
+ // Called on our SyncBackendHost's core_thread_ to perform authentication
+ // on behalf of SyncBackendHost::Authenticate.
+ void DoAuthenticate(const std::string& username,
+ const std::string& password);
+
+ // The shutdown order is a bit complicated:
+ // 1) From |core_thread_|, invoke the syncapi Shutdown call to do a final
+ // SaveChanges, close sqlite handles, and halt the syncer thread (which
+ // could potentially block for 1 minute).
+ // 2) Then, from |frontend_loop_|, halt the core_thread_. This causes
+ // syncapi thread-exit handlers to run and make use of cached pointers to
+ // various components owned implicitly by us.
+ // 3) Destroy this Core. That will delete syncapi components in a safe order
+ // because the thread that was using them has exited (in step 2).
+ void DoShutdown(bool stopping_sync);
+
+ sync_api::SyncManager* syncapi() { return syncapi_.get(); }
+
+#ifdef UNIT_TEST
+ // Special form of initialization that does not try and authenticate the
+ // last known user (since it will fail in test mode) and does some extra
+ // setup to nudge the syncapi into a useable state.
+ void DoInitializeForTest(BookmarkModelWorker* bookmark_model_worker,
+ const std::wstring& test_user) {
+ DoInitialize(GURL(), bookmark_model_worker, false);
+ syncapi_->SetupForTestMode(WideToUTF16(test_user).c_str());
+ }
+#endif
+
+ private:
+ // FrontendNotification defines parameters for NotifyFrontend. Each enum
+ // value corresponds to the one SyncFrontend interface method that
+ // NotifyFrontend should invoke.
+ enum FrontendNotification {
+ INITIALIZED, // OnBackendInitialized.
+ SYNC_CYCLE_COMPLETED, // A round-trip sync-cycle took place and
+ // the syncer has resolved any conflicts
+ // that may have arisen.
+ };
+
+ // NotifyFrontend is how the Core communicates with the frontend across
+ // threads. Having this extra method (rather than having the Core PostTask
+ // to the frontend explicitly) means SyncFrontend implementations don't
+ // need to be RefCountedThreadSafe because NotifyFrontend is invoked on the
+ // |frontend_loop_|.
+ void NotifyFrontend(FrontendNotification notification);
+
+ // Invoked when initialization of syncapi is complete and we can start
+ // our timer.
+ // This must be called from the thread on which SaveChanges is intended to
+ // be run on; the host's |core_thread_|.
+ void StartSavingChanges();
+
+ // Invoked periodically to tell the syncapi to persist its state
+ // by writing to disk.
+ // This is called from the thread we were created on (which is the
+ // SyncBackendHost |core_thread_|), using a repeating timer that is kicked
+ // off as soon as the SyncManager tells us it completed
+ // initialization.
+ void SaveChanges();
+
+ // Dispatched to from HandleAuthErrorEventOnCoreLoop to handle updating
+ // frontend UI components.
+ void HandleAuthErrorEventOnFrontendLoop(AuthErrorState new_auth_error);
+
+ // Our parent SyncBackendHost
+ SyncBackendHost* host_;
+
+ // The timer used to periodically call SaveChanges.
+ base::RepeatingTimer<Core> save_changes_timer_;
+
+ // The top-level syncapi entry point.
+ scoped_ptr<sync_api::SyncManager> syncapi_;
+
+ DISALLOW_COPY_AND_ASSIGN(Core);
+ };
+
+ // A thread we dedicate for use by our Core to perform initialization,
+ // authentication, handle messages from the syncapi, and periodically tell
+ // the syncapi to persist itself.
+ base::Thread core_thread_;
+
+ // Our core, which communicates directly to the syncapi.
+ scoped_refptr<Core> core_;
+
+ // A reference to the MessageLoop used to construct |this|, so we know how
+ // to safely talk back to the SyncFrontend.
+ MessageLoop* const frontend_loop_;
+
+ // We hold on to the BookmarkModelWorker created for the syncapi to ensure
+ // shutdown occurs in the sequence we expect by calling Stop() at the
+ // appropriate time. It is guaranteed to be valid because the worker is
+ // only destroyed when the SyncManager is destroyed, which happens when
+ // our Core is destroyed, which happens in Shutdown().
+ BookmarkModelWorker* bookmark_model_worker_;
+
+ // The frontend which we serve (and are owned by).
+ SyncFrontend* frontend_;
+
+ // Path of the folder that stores the sync data files.
+ FilePath sync_data_folder_path_;
+
+ // UI-thread cache of the last AuthErrorState received from syncapi.
+ AuthErrorState last_auth_error_;
+
+ DISALLOW_COPY_AND_ASSIGN(SyncBackendHost);
+};
+
+// SyncFrontend is the interface used by SyncBackendHost to communicate with
+// the entity that created it and, presumably, is interested in sync-related
+// activity.
+// NOTE: All methods will be invoked by a SyncBackendHost on the same thread
+// used to create that SyncBackendHost.
+class SyncFrontend {
+ public:
+ typedef sync_api::BaseTransaction BaseTransaction;
+ typedef sync_api::SyncManager::ChangeRecord ChangeRecord;
+ SyncFrontend() {
+ }
+
+ // The backend has completed initialization and it is now ready to accept and
+ // process changes.
+ virtual void OnBackendInitialized() = 0;
+
+ // The backend queried the server recently and received some updates.
+ virtual void OnSyncCycleCompleted() = 0;
+
+ // The backend encountered an authentication problem and requests new
+ // credentials to be provided. See SyncBackendHost::Authenticate for details.
+ virtual void OnAuthError() = 0;
+
+ // Changes have been applied to the backend model and are ready to be
+ // applied to the frontend model. See syncapi.h for detailed instructions on
+ // how to interpret and process |changes|.
+ virtual void ApplyModelChanges(const BaseTransaction* trans,
+ const ChangeRecord* changes,
+ int change_count) = 0;
+ protected:
+ // Don't delete through SyncFrontend interface.
+ virtual ~SyncFrontend() {
+ }
+ private:
+ DISALLOW_COPY_AND_ASSIGN(SyncFrontend);
+};
+
+} // namespace browser_sync
+
+#endif // CHROME_BROWSER_SYNC_GLUE_SYNC_BACKEND_HOST_H_
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/sync/personalization.cc b/chrome/browser/sync/personalization.cc
new file mode 100644
index 0000000..8e6a67c
--- /dev/null
+++ b/chrome/browser/sync/personalization.cc
@@ -0,0 +1,339 @@
+// Copyright (c) 2006-2008 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#include "chrome/browser/sync/personalization.h"
+
+#include "app/resource_bundle.h"
+#include "base/command_line.h"
+#include "base/file_path.h"
+#include "base/file_util.h"
+#include "base/path_service.h"
+#include "base/string_util.h"
+#include "chrome/app/chrome_dll_resource.h"
+#include "chrome/browser/browser.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/browser_url_handler.h"
+#include "chrome/browser/command_updater.h"
+#include "chrome/browser/options_window.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/profile_manager.h"
+#include "chrome/common/chrome_paths.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/common/jstemplate_builder.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/common/pref_service.h"
+#include "chrome/browser/dom_ui/new_tab_page_sync_handler.h"
+#include "chrome/browser/sync/personalization_strings.h"
+#include "chrome/browser/sync/auth_error_state.h"
+#include "chrome/browser/sync/profile_sync_service.h"
+#include "googleurl/src/gurl.h"
+#include "grit/app_resources.h"
+#include "grit/browser_resources.h"
+#include "net/url_request/url_request.h"
+
+using sync_api::SyncManager;
+
+// TODO(ncarter): Move these switches into chrome_switches. They are here
+// now because we want to keep them secret during early development.
+namespace switches {
+ const wchar_t kSyncServiceURL[] = L"sync-url";
+ const wchar_t kSyncServicePort[] = L"sync-port";
+ const wchar_t kSyncUserForTest[] = L"sync-user-for-test";
+ const wchar_t kSyncPasswordForTest[] = L"sync-password-for-test";
+}
+
+// TODO(munjal): Move these preferences to common/pref_names.h.
+// Names of various preferences.
+namespace prefs {
+ const wchar_t kSyncPath[] = L"sync";
+ const wchar_t kSyncLastSyncedTime[] = L"sync.last_synced_time";
+ const wchar_t kSyncUserName[] = L"sync.username";
+ const wchar_t kSyncHasSetupCompleted[] = L"sync.has_setup_completed";
+}
+
+// Top-level path for our network layer DataSource.
+static const char kCloudyResourcesPath[] = "resources";
+// Path for cloudy:stats page.
+static const char kCloudyStatsPath[] = "stats";
+// Path for the gaia sync login dialog.
+static const char kCloudyGaiaLoginPath[] = "gaialogin";
+static const char kCloudyMergeAndSyncPath[] = "mergeandsync";
+static const char kCloudyThrobberPath[] = "throbber.png";
+static const char kCloudySetupFlowPath[] = "setup";
+
+namespace Personalization {
+
+static std::wstring MakeAuthErrorText(AuthErrorState state) {
+ switch (state) {
+ case AUTH_ERROR_INVALID_GAIA_CREDENTIALS:
+ return L"INVALID_GAIA_CREDENTIALS";
+ case AUTH_ERROR_USER_NOT_SIGNED_UP:
+ return L"USER_NOT_SIGNED_UP";
+ case AUTH_ERROR_CONNECTION_FAILED:
+ return L"CONNECTION_FAILED";
+ default:
+ return std::wstring();
+ }
+}
+
+bool IsP13NDisabled(Profile* profile) {
+ const CommandLine* command_line = CommandLine::ForCurrentProcess();
+ if (command_line->HasSwitch(switches::kDisableP13n))
+ return true;
+ return !profile || profile->GetProfilePersonalization() == NULL;
+}
+
+bool NeedsDOMUI(const GURL& url) {
+ return url.SchemeIs(kPersonalizationScheme) &&
+ (url.path().find(kCloudyGaiaLoginPath) != std::string::npos) ||
+ (url.path().find(kCloudySetupFlowPath) != std::string::npos) ||
+ (url.path().find(kCloudyMergeAndSyncPath) != std::string::npos);
+}
+
+class CloudyResourceSource : public ChromeURLDataManager::DataSource {
+ public:
+ CloudyResourceSource()
+ : DataSource(kCloudyResourcesPath, MessageLoop::current()) {
+ }
+ virtual ~CloudyResourceSource() { }
+
+ virtual void StartDataRequest(const std::string& path, int request_id);
+
+ virtual std::string GetMimeType(const std::string& path) const {
+ if (path == kCloudyThrobberPath)
+ return "image/png";
+ else
+ return "text/html";
+ }
+ private:
+ DISALLOW_COPY_AND_ASSIGN(CloudyResourceSource);
+};
+
+class CloudyStatsSource : public ChromeURLDataManager::DataSource {
+ public:
+ CloudyStatsSource() : DataSource(kCloudyStatsPath, MessageLoop::current()) {
+ }
+ virtual ~CloudyStatsSource() { }
+ virtual void StartDataRequest(const std::string& path, int request_id) {
+ std::string response(MakeCloudyStats());
+ scoped_refptr<RefCountedBytes> html_bytes(new RefCountedBytes);
+ html_bytes->data.resize(response.size());
+ std::copy(response.begin(), response.end(), html_bytes->data.begin());
+ SendResponse(request_id, html_bytes);
+ }
+ virtual std::string GetMimeType(const std::string& path) const {
+ return "text/html";
+ }
+ private:
+ DISALLOW_COPY_AND_ASSIGN(CloudyStatsSource);
+};
+
+DOMMessageHandler* CreateNewTabPageHandler(DOMUI* dom_ui) {
+ return (new NewTabPageSyncHandler())->Attach(dom_ui);
+}
+
+std::string GetNewTabSource() {
+ static const StringPiece new_tab_html(
+ ResourceBundle::GetSharedInstance().GetRawDataResource(
+ IDR_NEW_TAB_P13N_HTML));
+
+ std::string data_uri("data:text/html,");
+ data_uri.append(std::string(new_tab_html.data(), new_tab_html.size()));
+ return GURL(data_uri).spec();
+}
+
+std::wstring GetMenuItemInfoText(Browser* browser) {
+ browser->command_updater()->UpdateCommandEnabled(IDC_P13N_INFO, true);
+ return kMenuLabelStartSync;
+}
+
+void HandleMenuItemClick(Profile* p) {
+ // The menu item is enabled either when the sync is not enabled by the user
+ // or when it's enabled but the user name is empty. In the former case enable
+ // sync. In the latter case, show the login dialog.
+ ProfileSyncService* service = p->GetProfilePersonalization()->sync_service();
+ DCHECK(service);
+ if (service->IsSyncEnabledByUser()) {
+ ShowOptionsWindow(OPTIONS_PAGE_USER_DATA, OPTIONS_GROUP_NONE, p);
+ } else {
+ service->EnableForUser();
+ }
+}
+
+} // namespace Personalization
+
+class ProfilePersonalizationImpl : public ProfilePersonalization,
+ public NotificationObserver {
+ public:
+ explicit ProfilePersonalizationImpl(Profile *p)
+ : profile_(p) {
+ // g_browser_process and/or io_thread may not exist during testing.
+ if (g_browser_process && g_browser_process->io_thread()) {
+ // Add our network layer data source for 'cloudy' URLs.
+ // TODO(timsteele): This one belongs in BrowserAboutHandler.
+ g_browser_process->io_thread()->message_loop()->PostTask(FROM_HERE,
+ NewRunnableMethod(&chrome_url_data_manager,
+ &ChromeURLDataManager::AddDataSource,
+ new Personalization::CloudyStatsSource()));
+ g_browser_process->io_thread()->message_loop()->PostTask(FROM_HERE,
+ NewRunnableMethod(&chrome_url_data_manager,
+ &ChromeURLDataManager::AddDataSource,
+ new Personalization::CloudyResourceSource()));
+ }
+
+ registrar_.Add(this, NotificationType::BOOKMARK_MODEL_LOADED,
+ Source<Profile>(profile_));
+ }
+ virtual ~ProfilePersonalizationImpl() {}
+
+ // ProfilePersonalization implementation
+ virtual ProfileSyncService* sync_service() {
+ if (!sync_service_.get())
+ InitSyncService();
+ return sync_service_.get();
+ }
+
+ // NotificationObserver implementation.
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ DCHECK_EQ(type.value, NotificationType::BOOKMARK_MODEL_LOADED);
+ if (!sync_service_.get())
+ InitSyncService();
+ registrar_.RemoveAll();
+ }
+
+ void InitSyncService() {
+ sync_service_.reset(new ProfileSyncService(profile_));
+ sync_service_->Initialize();
+ }
+
+ private:
+ Profile* profile_;
+ NotificationRegistrar registrar_;
+ scoped_ptr<ProfileSyncService> sync_service_;
+ DISALLOW_COPY_AND_ASSIGN(ProfilePersonalizationImpl);
+};
+
+namespace Personalization {
+
+void CloudyResourceSource::StartDataRequest(const std::string& path_raw,
+ int request_id) {
+ scoped_refptr<RefCountedBytes> html_bytes(new RefCountedBytes);
+ if (path_raw == kCloudyThrobberPath) {
+ ResourceBundle::GetSharedInstance().LoadImageResourceBytes(IDR_THROBBER,
+ &html_bytes->data);
+ SendResponse(request_id, html_bytes);
+ return;
+ }
+
+ std::string response;
+ if (path_raw == kCloudyGaiaLoginPath) {
+ static const StringPiece html(ResourceBundle::GetSharedInstance()
+ .GetRawDataResource(IDR_GAIA_LOGIN_HTML));
+ response = html.as_string();
+ } else if (path_raw == kCloudyMergeAndSyncPath) {
+ static const StringPiece html(ResourceBundle::GetSharedInstance()
+ .GetRawDataResource(IDR_MERGE_AND_SYNC_HTML));
+ response = html.as_string();
+ } else if (path_raw == kCloudySetupFlowPath) {
+ static const StringPiece html(ResourceBundle::GetSharedInstance()
+ .GetRawDataResource(IDR_SYNC_SETUP_FLOW_HTML));
+ response = html.as_string();
+ }
+ // Send the response.
+ html_bytes->data.resize(response.size());
+ std::copy(response.begin(), response.end(), html_bytes->data.begin());
+ SendResponse(request_id, html_bytes);
+}
+
+ProfilePersonalization* CreateProfilePersonalization(Profile* p) {
+ return new ProfilePersonalizationImpl(p);
+}
+
+void CleanupProfilePersonalization(ProfilePersonalization* p) {
+ if (p) delete p;
+}
+
+static void AddBoolDetail(ListValue* details, const std::wstring& stat_name,
+ bool stat_value) {
+ DictionaryValue* val = new DictionaryValue;
+ val->SetString(L"stat_name", stat_name);
+ val->SetBoolean(L"stat_value", stat_value);
+ details->Append(val);
+}
+
+static void AddIntDetail(ListValue* details, const std::wstring& stat_name,
+ int64 stat_value) {
+ DictionaryValue* val = new DictionaryValue;
+ val->SetString(L"stat_name", stat_name);
+ val->SetString(L"stat_value", FormatNumber(stat_value));
+ details->Append(val);
+}
+
+std::string MakeCloudyStats() {
+ FilePath user_data_dir;
+ if (!PathService::Get(chrome::DIR_USER_DATA, &user_data_dir))
+ return std::string();
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ Profile* profile = profile_manager->GetDefaultProfile(user_data_dir);
+ ProfilePersonalization* p13n_profile = profile->GetProfilePersonalization();
+ ProfileSyncService* service = p13n_profile->sync_service();
+
+ DictionaryValue strings;
+ if (!service->IsSyncEnabledByUser()) {
+ strings.SetString(L"summary", L"SYNC DISABLED");
+ } else {
+ SyncManager::Status full_status(service->QueryDetailedSyncStatus());
+
+ strings.SetString(L"summary",
+ ProfileSyncService::BuildSyncStatusSummaryText(
+ full_status.summary));
+
+ strings.Set(L"authenticated",
+ new FundamentalValue(full_status.authenticated));
+ strings.SetString(L"auth_problem",
+ MakeAuthErrorText(service->GetAuthErrorState()));
+
+ strings.SetString(L"time_since_sync", service->GetLastSyncedTimeString());
+
+ ListValue* details = new ListValue();
+ strings.Set(L"details", details);
+ AddBoolDetail(details, L"Server Up", full_status.server_up);
+ AddBoolDetail(details, L"Server Reachable", full_status.server_reachable);
+ AddBoolDetail(details, L"Server Broken", full_status.server_broken);
+ AddBoolDetail(details, L"Notifications Enabled",
+ full_status.notifications_enabled);
+ AddIntDetail(details, L"Notifications Received",
+ full_status.notifications_received);
+ AddIntDetail(details, L"Notifications Sent",
+ full_status.notifications_sent);
+ AddIntDetail(details, L"Unsynced Count", full_status.unsynced_count);
+ AddIntDetail(details, L"Conflicting Count", full_status.conflicting_count);
+ AddBoolDetail(details, L"Syncing", full_status.syncing);
+ AddBoolDetail(details, L"Syncer Paused", full_status.syncer_paused);
+ AddBoolDetail(details, L"Initial Sync Ended",
+ full_status.initial_sync_ended);
+ AddBoolDetail(details, L"Syncer Stuck", full_status.syncer_stuck);
+ AddIntDetail(details, L"Updates Available", full_status.updates_available);
+ AddIntDetail(details, L"Updates Received", full_status.updates_received);
+ AddBoolDetail(details, L"Disk Full", full_status.disk_full);
+ AddBoolDetail(details, L"Invalid Store", full_status.invalid_store);
+ AddIntDetail(details, L"Max Consecutive Errors",
+ full_status.max_consecutive_errors);
+ }
+
+ static const StringPiece sync_html(
+ ResourceBundle::GetSharedInstance().GetRawDataResource(
+ IDR_ABOUT_SYNC_HTML));
+
+ return jstemplate_builder::GetTemplateHtml(
+ sync_html, &strings , "t" /* template root node id */);
+}
+
+} // namespace Personalization
+
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/sync/personalization.h b/chrome/browser/sync/personalization.h
new file mode 100644
index 0000000..57decf5
--- /dev/null
+++ b/chrome/browser/sync/personalization.h
@@ -0,0 +1,109 @@
+// Copyright (c) 2006-2008 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+// TODO(timsteele): Remove this file by finding proper homes for everything in
+// trunk.
+#ifndef CHROME_BROWSER_SYNC_PERSONALIZATION_H_
+#define CHROME_BROWSER_SYNC_PERSONALIZATION_H_
+
+#include <string>
+#include "base/basictypes.h"
+#include "chrome/browser/dom_ui/chrome_url_data_manager.h"
+
+class Browser;
+class DOMUI;
+class DOMMessageHandler;
+class Profile;
+class RenderView;
+class RenderViewHost;
+class WebFrame;
+class WebView;
+
+class ProfileSyncService;
+class ProfileSyncServiceObserver;
+
+namespace views { class View; }
+
+// TODO(ncarter): Move these switches into chrome_switches. They are here
+// now because we want to keep them secret during early development.
+namespace switches {
+extern const wchar_t kSyncServiceURL[];
+extern const wchar_t kSyncServicePort[];
+extern const wchar_t kSyncUserForTest[];
+extern const wchar_t kSyncPasswordForTest[];
+}
+
+// Names of various preferences.
+// TODO(munjal): Move these preferences to common/pref_names.h.
+namespace prefs {
+extern const wchar_t kSyncPath[];
+extern const wchar_t kSyncLastSyncedTime[];
+extern const wchar_t kSyncUserName[];
+extern const wchar_t kSyncHasSetupCompleted[];
+}
+
+// Contains a profile sync service, which is initialized at profile creation.
+// A pointer to this class is passed as a handle.
+class ProfilePersonalization {
+ public:
+ ProfilePersonalization() {}
+ virtual ~ProfilePersonalization() {}
+
+ virtual ProfileSyncService* sync_service() = 0;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(ProfilePersonalization);
+};
+
+// Contains methods to perform Personalization-related tasks on behalf of the
+// caller.
+namespace Personalization {
+
+// Checks if P13N is globally disabled or not, and that |profile| has a valid
+// ProfilePersonalization member (it can be NULL for TestingProfiles).
+bool IsP13NDisabled(Profile* profile);
+
+// Returns whether |url| should be loaded in a DOMUI.
+bool NeedsDOMUI(const GURL& url);
+
+// Construct a new ProfilePersonalization and return it so the caller can take
+// ownership.
+ProfilePersonalization* CreateProfilePersonalization(Profile* p);
+
+// The caller of Create...() above should call this when the returned
+// ProfilePersonalization object should be deleted.
+void CleanupProfilePersonalization(ProfilePersonalization* p);
+
+// Handler for "cloudy:stats"
+std::string MakeCloudyStats();
+
+// Construct a new DOMMessageHandler for the new tab page |dom_ui|.
+DOMMessageHandler* CreateNewTabPageHandler(DOMUI* dom_ui);
+
+// Get HTML for the Personalization iframe in the New Tab Page.
+std::string GetNewTabSource();
+
+// Returns the text for personalization info menu item and sets its enabled
+// state.
+std::wstring GetMenuItemInfoText(Browser* browser);
+
+// Performs appropriate action when the sync menu item is clicked.
+void HandleMenuItemClick(Profile* p);
+} // namespace Personalization
+
+// The internal scheme used to retrieve HTML resources for personalization
+// related code (e.g cloudy:stats, GAIA login page).
+// We need to ensure the GAIA login HTML is loaded into an HTMLDialogContents.
+// Outside of p13n (for the time being) only "gears://" gives this (see
+// HtmlDialogContents::IsHtmlDialogUrl) for the application shortcut dialog.
+// TODO(timsteele): We should have a robust way to handle this to allow more
+// reuse of our HTML dialog code, perhaps by using a dedicated "dialog-resource"
+// scheme (chrome-resource is coupled to DOM_UI). Figure out if that is the best
+// course of action / pitch this idea to chromium-dev.
+static const char kPersonalizationScheme[] = "cloudy";
+
+#endif // CHROME_BROWSER_SYNC_PERSONALIZATION_H_
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/sync/personalization_strings.h b/chrome/browser/sync/personalization_strings.h
new file mode 100644
index 0000000..ed3036d
--- /dev/null
+++ b/chrome/browser/sync/personalization_strings.h
@@ -0,0 +1,69 @@
+// Copyright (c) 2006-2008 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.
+
+// Contains all string resources usec by the personalization module.
+
+// TODO(munjal): This file should go away once the personalization module
+// becomes public. At that point, we have to put all the strings below in the
+// generated resources file.
+
+#ifdef CHROME_PERSONALIZATION
+
+// TODO(timsteele): Rename this file; 'personalization' is deprecated.
+#ifndef CHROME_BROWSER_SYNC_PERSONALIZATION_STRINGS_H_
+#define CHROME_BROWSER_SYNC_PERSONALIZATION_STRINGS_H_
+
+// User Data tab in the Options menu.
+static const wchar_t kSyncGroupName[] = L"Bookmark Sync:";
+static const wchar_t kSyncNotSetupInfo[] =
+ L"You are not set up to sync your bookmarks with your other computers.";
+static const wchar_t kStartSyncButtonLabel[] = L"Synchronize my bookmarks...";
+static const wchar_t kSyncAccountLabel[] = L"Synced to ";
+static const wchar_t kLastSyncedLabel[] = L"Last synced: ";
+static const wchar_t kSyncCredentialsNeededLabel[] =
+ L"Account login details are not yet entered.";
+static const wchar_t kSyncAuthenticatingLabel[] = L"Authenticating...";
+static const wchar_t kSyncInvalidCredentialsError[] =
+ L"Invalid user name or password.";
+static const wchar_t kSyncOtherLoginErrorLabel[] =
+ L"Error signing in.";
+static const wchar_t kSyncExpiredCredentialsError[] =
+ L"Login details are out of date.";
+static const wchar_t kSyncServerNotReachableError[] =
+ L"Sync server is not reachable. Retrying...";
+static const wchar_t kSyncReLoginLinkLabel[] = L"Login again";
+static const wchar_t kStopSyncButtonLabel[] = L"Stop syncing this account";
+
+// Sync status messages.
+static const wchar_t kLastSyncedTimeNever[] = L"Never.";
+static const wchar_t kLastSyncedTimeWithinLastMinute[] = L"Just now.";
+
+// Sync merge warning dialog strings.
+static const wchar_t kMergeWarningMessageText[] =
+ L"WARNING: Your existing online bookmarks will be merged with the "
+ L"bookmarks on this machine. You can use the Bookmark Manager to organize "
+ L"your bookmarks after the merge.";
+static const wchar_t kCancelSyncButtonLabel[] = L"Cancel";
+static const wchar_t kMergeAndSyncButtonLabel[] = L"Merge and Sync";
+
+// Various strings for the new tab page personalization.
+static const char kSyncSectionTitle[] = "Bookmark Sync";
+static const char kSyncErrorSectionTitle[] = "Bookmark Sync Error!";
+static const char kSyncPromotionMsg[] =
+ "You can sync your bookmarks across computers using your Google account.";
+static const wchar_t kSyncServerUnavailableMsg[] =
+ L"Google Chrome could not sync your bookmarks because it could not connect "
+ L"to the sync server. Retrying...";
+static const char kStartNowLinkText[] = "Start now.";
+static const char kSettingUpText[] = "Setup in progress...";
+
+// Sync menu item strings.
+static const wchar_t kMenuLabelStartSync[] = L"Sync my bookmarks...";
+
+// Login dialog strings.
+static const wchar_t kLoginDialogTitle[] = L"Sync my bookmarks";
+
+#endif // CHROME_BROWSER_SYNC_PERSONALIZATION_STRINGS_H_
+
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/sync/profile_sync_service.cc b/chrome/browser/sync/profile_sync_service.cc
new file mode 100644
index 0000000..a832870
--- /dev/null
+++ b/chrome/browser/sync/profile_sync_service.cc
@@ -0,0 +1,887 @@
+// Copyright (c) 2006-2008 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.
+
+#ifdef CHROME_PERSONALIZATION
+#include "chrome/browser/sync/profile_sync_service.h"
+
+#include <stack>
+#include <vector>
+
+#include "base/basictypes.h"
+#include "base/command_line.h"
+#include "base/file_path.h"
+#include "base/file_util.h"
+#include "base/gfx/png_encoder.h"
+#include "base/stl_util-inl.h"
+#include "base/string_util.h"
+#include "base/time.h"
+#include "chrome/browser/bookmarks/bookmark_utils.h"
+#include "chrome/browser/history/history_notifications.h"
+#include "chrome/browser/history/history_types.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/sync/engine/syncapi.h"
+#include "chrome/browser/sync/personalization.h"
+#include "chrome/browser/sync/personalization_strings.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/common/pref_service.h"
+#include "chrome/common/time_format.h"
+#include "views/window/window.h"
+
+using browser_sync::ModelAssociator;
+using browser_sync::SyncBackendHost;
+
+ProfileSyncService::ProfileSyncService(Profile* profile)
+ : last_auth_error_(AUTH_ERROR_NONE),
+ profile_(profile),
+ backend_initialized_(false),
+ expecting_first_run_auth_needed_event_(false),
+ is_auth_in_progress_(false),
+ ready_to_process_changes_(false),
+ unrecoverable_error_detected_(false),
+ ALLOW_THIS_IN_INITIALIZER_LIST(wizard_(this)) {
+}
+
+ProfileSyncService::~ProfileSyncService() {
+ Shutdown(false);
+}
+
+void ProfileSyncService::Initialize() {
+ InitSettings();
+ RegisterPreferences();
+ if (!profile()->GetPrefs()->GetBoolean(prefs::kSyncHasSetupCompleted))
+ DisableForUser(); // Clean up in case of previous crash / setup abort.
+ else
+ StartUp();
+}
+
+void ProfileSyncService::InitSettings() {
+ const CommandLine& command_line = *CommandLine::ForCurrentProcess();
+
+ // Override the sync server URL from the command-line, if sync server and sync
+ // port command-line arguments exist.
+ if (command_line.HasSwitch(switches::kSyncServiceURL)) {
+ std::wstring value(command_line.GetSwitchValue(switches::kSyncServiceURL));
+ if (!value.empty()) {
+ GURL custom_sync_url(WideToUTF8(value));
+ if (custom_sync_url.is_valid()) {
+ sync_service_url_ = custom_sync_url;
+ } else {
+ LOG(WARNING) << "The following sync URL specified at the command-line "
+ << "is invalid: " << value;
+ }
+ }
+ } else {
+ NOTREACHED() << "--sync-url is required when sync is enabled.";
+ }
+
+ if (command_line.HasSwitch(switches::kSyncServicePort)) {
+ std::string port_str = WideToUTF8(command_line.GetSwitchValue(
+ switches::kSyncServicePort));
+ if (!port_str.empty()) {
+ GURL::Replacements replacements;
+ replacements.SetPortStr(port_str);
+ sync_service_url_ = sync_service_url_.ReplaceComponents(replacements);
+ }
+ }
+}
+
+void ProfileSyncService::RegisterPreferences() {
+ PrefService* pref_service = profile_->GetPrefs();
+ if (pref_service->IsPrefRegistered(prefs::kSyncUserName))
+ return;
+ pref_service->RegisterStringPref(prefs::kSyncUserName, std::wstring());
+ pref_service->RegisterStringPref(prefs::kSyncLastSyncedTime, std::wstring());
+ pref_service->RegisterBooleanPref(prefs::kSyncHasSetupCompleted, false);
+}
+
+void ProfileSyncService::LoadPreferences() {
+ PrefService* pref_service = profile_->GetPrefs();
+ std::wstring last_synced_time_string =
+ pref_service->GetString(prefs::kSyncLastSyncedTime);
+ if (!last_synced_time_string.empty()) {
+ int64 last_synced_time;
+ bool success = StringToInt64(WideToUTF16(last_synced_time_string),
+ &last_synced_time);
+ if (success) {
+ last_synced_time_ = base::Time::FromInternalValue(last_synced_time);
+ } else {
+ NOTREACHED();
+ }
+ }
+}
+
+void ProfileSyncService::ClearPreferences() {
+ PrefService* pref_service = profile_->GetPrefs();
+ pref_service->ClearPref(prefs::kSyncUserName);
+ pref_service->ClearPref(prefs::kSyncLastSyncedTime);
+ pref_service->ClearPref(prefs::kSyncHasSetupCompleted);
+
+ pref_service->ScheduleSavePersistentPrefs();
+}
+
+void ProfileSyncService::InitializeBackend() {
+ backend_->Initialize(sync_service_url_);
+}
+
+void ProfileSyncService::StartUp() {
+ // Don't start up multiple times.
+ if (backend_.get())
+ return;
+
+ LoadPreferences();
+
+ backend_.reset(new SyncBackendHost(this, profile_->GetPath()));
+
+ // We add ourselves as an observer, and we remain one forever. Note we don't
+ // keep any pointer to the model, we just receive notifications from it.
+ BookmarkModel* model = profile_->GetBookmarkModel();
+ model->AddObserver(this);
+
+ // Create new model assocation manager.
+ model_associator_ = new ModelAssociator(this);
+
+ // TODO(timsteele): HttpBridgeFactory should take a const* to the profile's
+ // URLRequestContext, because it needs it to create HttpBridge objects, and
+ // it may need to do that before the default request context has been set
+ // up. For now, call GetRequestContext lazy-init to force creation.
+ profile_->GetRequestContext();
+ InitializeBackend();
+}
+
+void ProfileSyncService::Shutdown(bool sync_disabled) {
+ if (backend_.get()) {
+ backend_->Shutdown(sync_disabled);
+ backend_.reset();
+ }
+
+ BookmarkModel* model = profile_->GetBookmarkModel();
+ if (model)
+ model->RemoveObserver(this);
+
+ // Clear all assocations and throw away the assocation manager instance.
+ if (model_associator_.get()) {
+ model_associator_->ClearAll();
+ model_associator_ = NULL;
+ }
+
+ // Clear various flags.
+ is_auth_in_progress_ = false;
+ backend_initialized_ = false;
+ expecting_first_run_auth_needed_event_ = false;
+ ready_to_process_changes_ = false;
+ last_attempted_user_email_.clear();
+}
+
+void ProfileSyncService::EnableForUser() {
+ if (wizard_.IsVisible()) {
+ // TODO(timsteele): Focus wizard.
+ return;
+ }
+ expecting_first_run_auth_needed_event_ = true;
+
+ StartUp();
+ FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged());
+}
+
+void ProfileSyncService::DisableForUser() {
+ if (wizard_.IsVisible()) {
+ // TODO(timsteele): Focus wizard.
+ return;
+ }
+ Shutdown(true);
+ ClearPreferences();
+
+ FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged());
+}
+
+void ProfileSyncService::Loaded(BookmarkModel* model) {
+ StartProcessingChangesIfReady();
+}
+
+void ProfileSyncService::UpdateSyncNodeProperties(const BookmarkNode* src,
+ sync_api::WriteNode* dst) {
+ // Set the properties of the item.
+ dst->SetIsFolder(src->is_folder());
+ dst->SetTitle(WideToUTF16(src->GetTitle()).c_str());
+ // URL is passed as a C string here because this interface avoids
+ // string16. SetURL copies the data into its own memory.
+ string16 url = UTF8ToUTF16(src->GetURL().spec());
+ dst->SetURL(url.c_str());
+ SetSyncNodeFavicon(src, dst);
+}
+
+void ProfileSyncService::EncodeFavicon(const BookmarkNode* src,
+ std::vector<unsigned char>* dst) const {
+ const SkBitmap& favicon = profile_->GetBookmarkModel()->GetFavIcon(src);
+
+ dst->clear();
+
+ // Check for zero-dimension images. This can happen if the favicon is
+ // still being loaded.
+ if (favicon.empty())
+ return;
+
+ // Re-encode the BookmarkNode's favicon as a PNG, and pass the data to the
+ // sync subsystem.
+ if (!PNGEncoder::EncodeBGRASkBitmap(favicon, false, dst))
+ return;
+}
+
+void ProfileSyncService::RemoveOneSyncNode(sync_api::WriteTransaction* trans,
+ const BookmarkNode* node) {
+ sync_api::WriteNode sync_node(trans);
+ if (!model_associator_->InitSyncNodeFromBookmarkId(node->id(), &sync_node)) {
+ SetUnrecoverableError();
+ return;
+ }
+ // This node should have no children.
+ DCHECK(sync_node.GetFirstChildId() == sync_api::kInvalidId);
+ // Remove association and delete the sync node.
+ model_associator_->DisassociateIds(sync_node.GetId());
+ sync_node.Remove();
+}
+
+void ProfileSyncService::RemoveSyncNodeHierarchy(const BookmarkNode* topmost) {
+ sync_api::WriteTransaction trans(backend_->GetUserShareHandle());
+
+ // Later logic assumes that |topmost| has been unlinked.
+ DCHECK(!topmost->GetParent());
+
+ // A BookmarkModel deletion event means that |node| and all its children were
+ // deleted. Sync backend expects children to be deleted individually, so we do
+ // a depth-first-search here. At each step, we consider the |index|-th child
+ // of |node|. |index_stack| stores index values for the parent levels.
+ std::stack<int> index_stack;
+ index_stack.push(0); // For the final pop. It's never used.
+ const BookmarkNode* node = topmost;
+ int index = 0;
+ while (node) {
+ // The top of |index_stack| should always be |node|'s index.
+ DCHECK(!node->GetParent() || (node->GetParent()->IndexOfChild(node) ==
+ index_stack.top()));
+ if (index == node->GetChildCount()) {
+ // If we've processed all of |node|'s children, delete |node| and move
+ // on to its successor.
+ RemoveOneSyncNode(&trans, node);
+ node = node->GetParent();
+ index = index_stack.top() + 1; // (top() + 0) was what we removed.
+ index_stack.pop();
+ } else {
+ // If |node| has an unprocessed child, process it next after pushing the
+ // current state onto the stack.
+ DCHECK_LT(index, node->GetChildCount());
+ index_stack.push(index);
+ node = node->GetChild(index);
+ index = 0;
+ }
+ }
+ DCHECK(index_stack.empty()); // Nothing should be left on the stack.
+}
+
+bool ProfileSyncService::MergeAndSyncAcceptanceNeeded() const {
+ // If we've shown the dialog before, don't show it again.
+ if (profile_->GetPrefs()->GetBoolean(prefs::kSyncHasSetupCompleted))
+ return false;
+
+ return model_associator_->BookmarkModelHasUserCreatedNodes() &&
+ model_associator_->SyncModelHasUserCreatedNodes();
+}
+
+bool ProfileSyncService::IsSyncEnabledByUser() const {
+ return profile_->GetPrefs()->GetBoolean(prefs::kSyncHasSetupCompleted);
+}
+
+void ProfileSyncService::UpdateLastSyncedTime() {
+ last_synced_time_ = base::Time::Now();
+ profile_->GetPrefs()->SetString(prefs::kSyncLastSyncedTime,
+ Int64ToWString(last_synced_time_.ToInternalValue()));
+}
+
+void ProfileSyncService::BookmarkNodeAdded(BookmarkModel* model,
+ const BookmarkNode* parent,
+ int index) {
+ if (!ShouldPushChanges())
+ return;
+
+ DCHECK(backend_->GetUserShareHandle());
+
+ // Acquire a scoped write lock via a transaction.
+ sync_api::WriteTransaction trans(backend_->GetUserShareHandle());
+
+ CreateSyncNode(parent, index, &trans);
+}
+
+int64 ProfileSyncService::CreateSyncNode(const BookmarkNode* parent,
+ int index,
+ sync_api::WriteTransaction* trans) {
+ const BookmarkNode* child = parent->GetChild(index);
+ DCHECK(child);
+
+ // Create a WriteNode container to hold the new node.
+ sync_api::WriteNode sync_child(trans);
+
+ // Actually create the node with the appropriate initial position.
+ if (!PlaceSyncNode(CREATE, parent, index, trans, &sync_child)) {
+ LOG(WARNING) << "Sync node creation failed; recovery unlikely";
+ SetUnrecoverableError();
+ return sync_api::kInvalidId;
+ }
+
+ UpdateSyncNodeProperties(child, &sync_child);
+
+ // Associate the ID from the sync domain with the bookmark node, so that we
+ // can refer back to this item later.
+ model_associator_->AssociateIds(child->id(), sync_child.GetId());
+
+ return sync_child.GetId();
+}
+
+void ProfileSyncService::BookmarkNodeRemoved(BookmarkModel* model,
+ const BookmarkNode* parent,
+ int index,
+ const BookmarkNode* node) {
+ if (!ShouldPushChanges())
+ return;
+
+ RemoveSyncNodeHierarchy(node);
+}
+
+void ProfileSyncService::BookmarkNodeChanged(BookmarkModel* model,
+ const BookmarkNode* node) {
+ if (!ShouldPushChanges())
+ return;
+
+ // We shouldn't see changes to the top-level nodes.
+ DCHECK_NE(node, model->GetBookmarkBarNode());
+ DCHECK_NE(node, model->other_node());
+
+ // Acquire a scoped write lock via a transaction.
+ sync_api::WriteTransaction trans(backend_->GetUserShareHandle());
+
+ // Lookup the sync node that's associated with |node|.
+ sync_api::WriteNode sync_node(&trans);
+ if (!model_associator_->InitSyncNodeFromBookmarkId(node->id(), &sync_node)) {
+ SetUnrecoverableError();
+ return;
+ }
+
+ UpdateSyncNodeProperties(node, &sync_node);
+
+ DCHECK_EQ(sync_node.GetIsFolder(), node->is_folder());
+ DCHECK_EQ(model_associator_->GetBookmarkNodeFromSyncId(
+ sync_node.GetParentId()),
+ node->GetParent());
+ // This node's index should be one more than the predecessor's index.
+ DCHECK_EQ(node->GetParent()->IndexOfChild(node),
+ CalculateBookmarkModelInsertionIndex(node->GetParent(),
+ &sync_node));
+}
+
+void ProfileSyncService::BookmarkNodeMoved(BookmarkModel* model,
+ const BookmarkNode* old_parent,
+ int old_index,
+ const BookmarkNode* new_parent,
+ int new_index) {
+ if (!ShouldPushChanges())
+ return;
+
+ const BookmarkNode* child = new_parent->GetChild(new_index);
+ // We shouldn't see changes to the top-level nodes.
+ DCHECK_NE(child, model->GetBookmarkBarNode());
+ DCHECK_NE(child, model->other_node());
+
+ // Acquire a scoped write lock via a transaction.
+ sync_api::WriteTransaction trans(backend_->GetUserShareHandle());
+
+ // Lookup the sync node that's associated with |child|.
+ sync_api::WriteNode sync_node(&trans);
+ if (!model_associator_->InitSyncNodeFromBookmarkId(child->id(),
+ &sync_node)) {
+ SetUnrecoverableError();
+ return;
+ }
+
+ if (!PlaceSyncNode(MOVE, new_parent, new_index, &trans, &sync_node)) {
+ SetUnrecoverableError();
+ return;
+ }
+}
+
+void ProfileSyncService::BookmarkNodeFavIconLoaded(BookmarkModel* model,
+ const BookmarkNode* node) {
+ BookmarkNodeChanged(model, node);
+}
+
+void ProfileSyncService::BookmarkNodeChildrenReordered(
+ BookmarkModel* model, const BookmarkNode* node) {
+ if (!ShouldPushChanges())
+ return;
+
+ // Acquire a scoped write lock via a transaction.
+ sync_api::WriteTransaction trans(backend_->GetUserShareHandle());
+
+ // The given node's children got reordered. We need to reorder all the
+ // children of the corresponding sync node.
+ for (int i = 0; i < node->GetChildCount(); ++i) {
+ sync_api::WriteNode sync_child(&trans);
+ if (!model_associator_->InitSyncNodeFromBookmarkId(node->GetChild(i)->id(),
+ &sync_child)) {
+ SetUnrecoverableError();
+ return;
+ }
+ DCHECK_EQ(sync_child.GetParentId(),
+ model_associator_->GetSyncIdFromBookmarkId(node->id()));
+
+ if (!PlaceSyncNode(MOVE, node, i, &trans, &sync_child)) {
+ SetUnrecoverableError();
+ return;
+ }
+ }
+}
+
+bool ProfileSyncService::PlaceSyncNode(MoveOrCreate operation,
+ const BookmarkNode* parent,
+ int index,
+ sync_api::WriteTransaction* trans,
+ sync_api::WriteNode* dst) {
+ sync_api::ReadNode sync_parent(trans);
+ if (!model_associator_->InitSyncNodeFromBookmarkId(parent->id(),
+ &sync_parent)) {
+ LOG(WARNING) << "Parent lookup failed";
+ SetUnrecoverableError();
+ return false;
+ }
+
+ bool success = false;
+ if (index == 0) {
+ // Insert into first position.
+ success = (operation == CREATE) ? dst->InitByCreation(sync_parent, NULL) :
+ dst->SetPosition(sync_parent, NULL);
+ if (success) {
+ DCHECK_EQ(dst->GetParentId(), sync_parent.GetId());
+ DCHECK_EQ(dst->GetId(), sync_parent.GetFirstChildId());
+ DCHECK_EQ(dst->GetPredecessorId(), sync_api::kInvalidId);
+ }
+ } else {
+ // Find the bookmark model predecessor, and insert after it.
+ const BookmarkNode* prev = parent->GetChild(index - 1);
+ sync_api::ReadNode sync_prev(trans);
+ if (!model_associator_->InitSyncNodeFromBookmarkId(prev->id(),
+ &sync_prev)) {
+ LOG(WARNING) << "Predecessor lookup failed";
+ return false;
+ }
+ success = (operation == CREATE) ?
+ dst->InitByCreation(sync_parent, &sync_prev) :
+ dst->SetPosition(sync_parent, &sync_prev);
+ if (success) {
+ DCHECK_EQ(dst->GetParentId(), sync_parent.GetId());
+ DCHECK_EQ(dst->GetPredecessorId(), sync_prev.GetId());
+ DCHECK_EQ(dst->GetId(), sync_prev.GetSuccessorId());
+ }
+ }
+ return success;
+}
+
+// An invariant has been violated. Transition to an error state where we try
+// to do as little work as possible, to avoid further corruption or crashes.
+void ProfileSyncService::SetUnrecoverableError() {
+ unrecoverable_error_detected_ = true;
+ LOG(ERROR) << "Unrecoverable error detected -- ProfileSyncService unusable.";
+}
+
+// Determine the bookmark model index to which a node must be moved so that
+// predecessor of the node (in the bookmark model) matches the predecessor of
+// |source| (in the sync model).
+// As a precondition, this assumes that the predecessor of |source| has been
+// updated and is already in the correct position in the bookmark model.
+int ProfileSyncService::CalculateBookmarkModelInsertionIndex(
+ const BookmarkNode* parent,
+ const sync_api::BaseNode* child_info) const {
+ DCHECK(parent);
+ DCHECK(child_info);
+ int64 predecessor_id = child_info->GetPredecessorId();
+ // A return ID of kInvalidId indicates no predecessor.
+ if (predecessor_id == sync_api::kInvalidId)
+ return 0;
+
+ // Otherwise, insert after the predecessor bookmark node.
+ const BookmarkNode* predecessor =
+ model_associator_->GetBookmarkNodeFromSyncId(predecessor_id);
+ DCHECK(predecessor);
+ DCHECK_EQ(predecessor->GetParent(), parent);
+ return parent->IndexOfChild(predecessor) + 1;
+}
+
+void ProfileSyncService::OnBackendInitialized() {
+ backend_initialized_ = true;
+
+ PrefService* pref_service = profile_->GetPrefs();
+ DCHECK(pref_service->IsPrefRegistered(prefs::kSyncUserName));
+ pref_service->SetString(prefs::kSyncUserName,
+ UTF16ToWide(backend_->GetAuthenticatedUsername()));
+ StartProcessingChangesIfReady();
+
+ // The very first time the backend initializes is effectively the first time
+ // we can say we successfully "synced". last_synced_time_ will only be null
+ // in this case, because the pref wasn't restored on StartUp.
+ if (last_synced_time_.is_null())
+ UpdateLastSyncedTime();
+ FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged());
+}
+
+void ProfileSyncService::OnSyncCycleCompleted() {
+ UpdateLastSyncedTime();
+ FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged());
+}
+
+void ProfileSyncService::OnAuthError() {
+ last_auth_error_ = backend_->GetAuthErrorState();
+ // Protect against the in-your-face dialogs that pop out of nowhere.
+ // Require the user to click somewhere to run the setup wizard in the case
+ // of a steady-state auth failure.
+ if (wizard_.IsVisible() || expecting_first_run_auth_needed_event_) {
+ wizard_.Step(AUTH_ERROR_NONE == backend_->GetAuthErrorState() ?
+ SyncSetupWizard::GAIA_SUCCESS : SyncSetupWizard::GAIA_LOGIN);
+ }
+
+ if (expecting_first_run_auth_needed_event_) {
+ last_auth_error_ = AUTH_ERROR_NONE;
+ expecting_first_run_auth_needed_event_ = false;
+ }
+
+ is_auth_in_progress_ = false;
+ // Fan the notification out to interested UI-thread components.
+ FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged());
+}
+
+void ProfileSyncService::ShowLoginDialog() {
+ if (wizard_.IsVisible())
+ return;
+ if (last_auth_error_ != AUTH_ERROR_NONE)
+ wizard_.Step(SyncSetupWizard::GAIA_LOGIN);
+}
+
+// ApplyModelChanges is called by the sync backend after changes have been made
+// to the sync engine's model. Apply these changes to the browser bookmark
+// model.
+void ProfileSyncService::ApplyModelChanges(
+ const sync_api::BaseTransaction* trans,
+ const sync_api::SyncManager::ChangeRecord* changes,
+ int change_count) {
+ if (!ShouldPushChanges())
+ return;
+
+ // A note about ordering. Sync backend is responsible for ordering the change
+ // records in the following order:
+ //
+ // 1. Deletions, from leaves up to parents.
+ // 2. Existing items with synced parents & predecessors.
+ // 3. New items with synced parents & predecessors.
+ // 4. Items with parents & predecessors in the list.
+ // 5. Repeat #4 until all items are in the list.
+ //
+ // "Predecessor" here means the previous item within a given folder; an item
+ // in the first position is always said to have a synced predecessor.
+ // For the most part, applying these changes in the order given will yield
+ // the correct result. There is one exception, however: for items that are
+ // moved away from a folder that is being deleted, we will process the delete
+ // before the move. Since deletions in the bookmark model propagate from
+ // parent to child, we must move them to a temporary location.
+ BookmarkModel* model = profile_->GetBookmarkModel();
+
+ // We are going to make changes to the bookmarks model, but don't want to end
+ // up in a feedback loop, so remove ourselves as an observer while applying
+ // changes.
+ model->RemoveObserver(this);
+
+ // A parent to hold nodes temporarily orphaned by parent deletion. It is
+ // lazily created inside the loop.
+ const BookmarkNode* foster_parent = NULL;
+ for (int i = 0; i < change_count; ++i) {
+ const BookmarkNode* dst =
+ model_associator_->GetBookmarkNodeFromSyncId(changes[i].id);
+ // Ignore changes to the permanent top-level nodes. We only care about
+ // their children.
+ if ((dst == model->GetBookmarkBarNode()) || (dst == model->other_node()))
+ continue;
+ if (changes[i].action ==
+ sync_api::SyncManager::ChangeRecord::ACTION_DELETE) {
+ // Deletions should always be at the front of the list.
+ DCHECK(i == 0 || changes[i-1].action == changes[i].action);
+ // Children of a deleted node should not be deleted; they may be
+ // reparented by a later change record. Move them to a temporary place.
+ DCHECK(dst) << "Could not find node to be deleted";
+ const BookmarkNode* parent = dst->GetParent();
+ if (dst->GetChildCount()) {
+ if (!foster_parent) {
+ foster_parent = model->AddGroup(model->other_node(),
+ model->other_node()->GetChildCount(),
+ std::wstring());
+ }
+ for (int i = dst->GetChildCount() - 1; i >= 0; --i) {
+ model->Move(dst->GetChild(i), foster_parent,
+ foster_parent->GetChildCount());
+ }
+ }
+ DCHECK_EQ(dst->GetChildCount(), 0) << "Node being deleted has children";
+ model->Remove(parent, parent->IndexOfChild(dst));
+ dst = NULL;
+ model_associator_->DisassociateIds(changes[i].id);
+ } else {
+ DCHECK_EQ((changes[i].action ==
+ sync_api::SyncManager::ChangeRecord::ACTION_ADD), (dst == NULL))
+ << "ACTION_ADD should be seen if and only if the node is unknown.";
+
+ sync_api::ReadNode src(trans);
+ if (!src.InitByIdLookup(changes[i].id)) {
+ LOG(ERROR) << "ApplyModelChanges was passed a bad ID";
+ SetUnrecoverableError();
+ return;
+ }
+
+ CreateOrUpdateBookmarkNode(&src, model);
+ }
+ }
+ // Clean up the temporary node.
+ if (foster_parent) {
+ // There should be no nodes left under the foster parent.
+ DCHECK_EQ(foster_parent->GetChildCount(), 0);
+ model->Remove(foster_parent->GetParent(),
+ foster_parent->GetParent()->IndexOfChild(foster_parent));
+ foster_parent = NULL;
+ }
+
+ // We are now ready to hear about bookmarks changes again.
+ model->AddObserver(this);
+}
+
+// Create a bookmark node corresponding to |src| if one is not already
+// associated with |src|.
+const BookmarkNode* ProfileSyncService::CreateOrUpdateBookmarkNode(
+ sync_api::BaseNode* src,
+ BookmarkModel* model) {
+ const BookmarkNode* parent =
+ model_associator_->GetBookmarkNodeFromSyncId(src->GetParentId());
+ if (!parent) {
+ DLOG(WARNING) << "Could not find parent of node being added/updated."
+ << " Node title: " << src->GetTitle()
+ << ", parent id = " << src->GetParentId();
+ return NULL;
+ }
+ int index = CalculateBookmarkModelInsertionIndex(parent, src);
+ const BookmarkNode* dst = model_associator_->GetBookmarkNodeFromSyncId(
+ src->GetId());
+ if (!dst) {
+ dst = CreateBookmarkNode(src, parent, index);
+ model_associator_->AssociateIds(dst->id(), src->GetId());
+ } else {
+ // URL and is_folder are not expected to change.
+ // TODO(ncarter): Determine if such changes should be legal or not.
+ DCHECK_EQ(src->GetIsFolder(), dst->is_folder());
+
+ // Handle reparenting and/or repositioning.
+ model->Move(dst, parent, index);
+
+ // Handle title update and URL changes due to possible conflict resolution
+ // that can happen if both a local user change and server change occur
+ // within a sufficiently small time interval.
+ const BookmarkNode* old_dst = dst;
+ dst = bookmark_utils::ApplyEditsWithNoGroupChange(model, parent, dst,
+ UTF16ToWide(src->GetTitle()),
+ src->GetIsFolder() ? GURL() : GURL(src->GetURL()),
+ NULL); // NULL because we don't need a BookmarkEditor::Handler.
+ if (dst != old_dst) { // dst was replaced with a new node with new URL.
+ model_associator_->DisassociateIds(src->GetId());
+ model_associator_->AssociateIds(dst->id(), src->GetId());
+ }
+ SetBookmarkFavicon(src, dst);
+ }
+
+ return dst;
+}
+
+// Creates a bookmark node under the given parent node from the given sync
+// node. Returns the newly created node.
+const BookmarkNode* ProfileSyncService::CreateBookmarkNode(
+ sync_api::BaseNode* sync_node,
+ const BookmarkNode* parent,
+ int index) const {
+ DCHECK(parent);
+ DCHECK(index >= 0 && index <= parent->GetChildCount());
+ BookmarkModel* model = profile_->GetBookmarkModel();
+
+ const BookmarkNode* node;
+ if (sync_node->GetIsFolder()) {
+ node = model->AddGroup(parent, index, UTF16ToWide(sync_node->GetTitle()));
+ } else {
+ GURL url(sync_node->GetURL());
+ node = model->AddURL(parent, index,
+ UTF16ToWide(sync_node->GetTitle()), url);
+ SetBookmarkFavicon(sync_node, node);
+ }
+ return node;
+}
+
+// Sets the favicon of the given bookmark node from the given sync node.
+bool ProfileSyncService::SetBookmarkFavicon(
+ sync_api::BaseNode* sync_node,
+ const BookmarkNode* bookmark_node) const {
+ size_t icon_size = 0;
+ const unsigned char* icon_bytes = sync_node->GetFaviconBytes(&icon_size);
+ if (!icon_size || !icon_bytes)
+ return false;
+
+ // Registering a favicon requires that we provide a source URL, but we
+ // don't know where these came from. Currently we just use the
+ // destination URL, which is not correct, but since the favicon URL
+ // is used as a key in the history's thumbnail DB, this gives us a value
+ // which does not collide with others.
+ GURL fake_icon_url = bookmark_node->GetURL();
+
+ std::vector<unsigned char> icon_bytes_vector(icon_bytes,
+ icon_bytes + icon_size);
+
+ HistoryService* history =
+ profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
+
+ history->AddPage(bookmark_node->GetURL());
+ history->SetFavIcon(bookmark_node->GetURL(),
+ fake_icon_url,
+ icon_bytes_vector);
+
+ return true;
+}
+
+void ProfileSyncService::SetSyncNodeFavicon(
+ const BookmarkNode* bookmark_node,
+ sync_api::WriteNode* sync_node) const {
+ std::vector<unsigned char> favicon_bytes;
+ EncodeFavicon(bookmark_node, &favicon_bytes);
+ if (!favicon_bytes.empty())
+ sync_node->SetFaviconBytes(&favicon_bytes[0], favicon_bytes.size());
+}
+
+SyncBackendHost::StatusSummary ProfileSyncService::QuerySyncStatusSummary() {
+ return backend_->GetStatusSummary();
+}
+
+SyncBackendHost::Status ProfileSyncService::QueryDetailedSyncStatus() {
+ return backend_->GetDetailedStatus();
+}
+
+std::wstring ProfileSyncService::BuildSyncStatusSummaryText(
+ const sync_api::SyncManager::Status::Summary& summary) {
+ switch (summary) {
+ case sync_api::SyncManager::Status::OFFLINE:
+ return L"OFFLINE";
+ case sync_api::SyncManager::Status::OFFLINE_UNSYNCED:
+ return L"OFFLINE_UNSYNCED";
+ case sync_api::SyncManager::Status::SYNCING:
+ return L"SYNCING";
+ case sync_api::SyncManager::Status::READY:
+ return L"READY";
+ case sync_api::SyncManager::Status::PAUSED:
+ return L"PAUSED";
+ case sync_api::SyncManager::Status::CONFLICT:
+ return L"CONFLICT";
+ case sync_api::SyncManager::Status::OFFLINE_UNUSABLE:
+ return L"OFFLINE_UNUSABLE";
+ case sync_api::SyncManager::Status::INVALID: // fall through
+ default:
+ return L"UNKNOWN";
+ }
+}
+
+std::wstring ProfileSyncService::GetLastSyncedTimeString() const {
+ if (last_synced_time_.is_null())
+ return kLastSyncedTimeNever;
+
+ base::TimeDelta last_synced = base::Time::Now() - last_synced_time_;
+
+ if (last_synced < base::TimeDelta::FromMinutes(1))
+ return kLastSyncedTimeWithinLastMinute;
+
+ return TimeFormat::TimeElapsed(last_synced);
+}
+
+string16 ProfileSyncService::GetAuthenticatedUsername() const {
+ return backend_->GetAuthenticatedUsername();
+}
+
+void ProfileSyncService::OnUserSubmittedAuth(
+ const std::string& username, const std::string& password) {
+ last_attempted_user_email_ = username;
+ is_auth_in_progress_ = true;
+ FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged());
+ backend_->Authenticate(username, password);
+}
+
+void ProfileSyncService::OnUserAcceptedMergeAndSync() {
+ bool merge_success = model_associator_->AssociateModels();
+ wizard_.Step(SyncSetupWizard::DONE); // TODO(timsteele): error state?
+ if (!merge_success) {
+ LOG(ERROR) << "Model assocation failed.";
+ SetUnrecoverableError();
+ return;
+ }
+
+ ready_to_process_changes_ = true;
+ FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged());
+}
+
+void ProfileSyncService::OnUserCancelledDialog() {
+ if (!profile_->GetPrefs()->GetBoolean(prefs::kSyncHasSetupCompleted)) {
+ // A sync dialog was aborted before authentication or merge acceptance.
+ // Rollback.
+ DisableForUser();
+ }
+ FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged());
+}
+
+void ProfileSyncService::StartProcessingChangesIfReady() {
+ BookmarkModel* model = profile_->GetBookmarkModel();
+
+ DCHECK(!ready_to_process_changes_);
+
+ // First check if the subsystems are ready. We can't proceed until they
+ // both have finished loading.
+ if (!model->IsLoaded())
+ return;
+ if (!backend_initialized_)
+ return;
+
+ // Show the sync merge warning dialog if needed.
+ if (MergeAndSyncAcceptanceNeeded()) {
+ wizard_.Step(SyncSetupWizard::MERGE_AND_SYNC);
+ return;
+ }
+
+ // We're ready to merge the models.
+ bool merge_success = model_associator_->AssociateModels();
+ wizard_.Step(SyncSetupWizard::DONE); // TODO(timsteele): error state?
+ if (!merge_success) {
+ LOG(ERROR) << "Model assocation failed.";
+ SetUnrecoverableError();
+ return;
+ }
+
+ ready_to_process_changes_ = true;
+ FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged());
+}
+
+void ProfileSyncService::AddObserver(Observer* observer) {
+ observers_.AddObserver(observer);
+}
+
+void ProfileSyncService::RemoveObserver(Observer* observer) {
+ observers_.RemoveObserver(observer);
+}
+
+bool ProfileSyncService::ShouldPushChanges() {
+ return ready_to_process_changes_ && // Wait for model load and merge.
+ !unrecoverable_error_detected_; // Halt after any terrible events.
+}
+
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/sync/profile_sync_service.h b/chrome/browser/sync/profile_sync_service.h
new file mode 100644
index 0000000..d91edbe
--- /dev/null
+++ b/chrome/browser/sync/profile_sync_service.h
@@ -0,0 +1,368 @@
+// Copyright (c) 2006-2008 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#ifndef CHROME_BROWSER_SYNC_PROFILE_SYNC_SERVICE_H_
+#define CHROME_BROWSER_SYNC_PROFILE_SYNC_SERVICE_H_
+
+#include <string>
+#include <map>
+#include <vector>
+
+#include "base/basictypes.h"
+#include "base/file_path.h"
+#include "base/observer_list.h"
+#include "base/scoped_ptr.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/sync/glue/model_associator.h"
+#include "chrome/browser/sync/glue/sync_backend_host.h"
+#include "chrome/browser/views/sync/sync_setup_wizard.h"
+#include "googleurl/src/gurl.h"
+
+class CommandLine;
+class MessageLoop;
+class Profile;
+
+namespace browser_sync {
+class ModelAssociator;
+}
+
+// Various UI components such as the New Tab page can be driven by observing
+// the ProfileSyncService through this interface.
+class ProfileSyncServiceObserver {
+ public:
+ // When one of the following events occurs, OnStateChanged() is called.
+ // Observers should query the service to determine what happened.
+ // - We initialized successfully.
+ // - There was an authentication error and the user needs to reauthenticate.
+ // - The sync servers are unavailable at this time.
+ // - Credentials are now in flight for authentication.
+ virtual void OnStateChanged() = 0;
+ protected:
+ virtual ~ProfileSyncServiceObserver() { }
+};
+
+// ProfileSyncService is the layer between browser subsystems like bookmarks,
+// and the sync backend.
+class ProfileSyncService : public BookmarkModelObserver,
+ public browser_sync::SyncFrontend {
+ public:
+ typedef ProfileSyncServiceObserver Observer;
+ typedef browser_sync::SyncBackendHost::Status Status;
+
+ explicit ProfileSyncService(Profile* profile);
+ virtual ~ProfileSyncService();
+
+ // Initializes the object. This should be called every time an object of this
+ // class is constructed.
+ void Initialize();
+
+ // Enables/disables sync for user.
+ virtual void EnableForUser();
+ virtual void DisableForUser();
+
+ // Whether sync is enabled by user or not.
+ bool IsSyncEnabledByUser() const;
+
+ // BookmarkModelObserver implementation.
+ virtual void Loaded(BookmarkModel* model);
+ virtual void BookmarkModelBeingDeleted(BookmarkModel* model) {}
+ virtual void BookmarkNodeMoved(BookmarkModel* model,
+ const BookmarkNode* old_parent,
+ int old_index,
+ const BookmarkNode* new_parent,
+ int new_index);
+ virtual void BookmarkNodeAdded(BookmarkModel* model,
+ const BookmarkNode* parent,
+ int index);
+ virtual void BookmarkNodeRemoved(BookmarkModel* model,
+ const BookmarkNode* parent,
+ int index,
+ const BookmarkNode* node);
+ virtual void BookmarkNodeChanged(BookmarkModel* model,
+ const BookmarkNode* node);
+ virtual void BookmarkNodeFavIconLoaded(BookmarkModel* model,
+ const BookmarkNode* node);
+ virtual void BookmarkNodeChildrenReordered(BookmarkModel* model,
+ const BookmarkNode* node);
+
+ // SyncFrontend implementation.
+ virtual void OnBackendInitialized();
+ virtual void OnSyncCycleCompleted();
+ virtual void OnAuthError();
+ virtual void ApplyModelChanges(
+ const sync_api::BaseTransaction* trans,
+ const sync_api::SyncManager::ChangeRecord* changes,
+ int change_count);
+
+ // Called when a user enters credentials through UI.
+ virtual void OnUserSubmittedAuth(const std::string& username,
+ const std::string& password);
+
+ // Called when a user decides whether to merge and sync or abort.
+ virtual void OnUserAcceptedMergeAndSync();
+
+ // Called when a user cancels any setup dialog (login, merge and sync, etc).
+ virtual void OnUserCancelledDialog();
+
+ // Get various information for displaying in the user interface.
+ browser_sync::SyncBackendHost::StatusSummary QuerySyncStatusSummary();
+ browser_sync::SyncBackendHost::Status QueryDetailedSyncStatus();
+
+ AuthErrorState GetAuthErrorState() const {
+ return last_auth_error_;
+ }
+
+ // Displays a dialog for the user to enter GAIA credentials and attempt
+ // re-authentication, and returns true if it actually opened the dialog.
+ // Returns false if a dialog is already showing, an auth attempt is in
+ // progress, the sync system is already authenticated, or some error
+ // occurred preventing the action. We make it the duty of ProfileSyncService
+ // to open the dialog to easily ensure only one is ever showing.
+ bool SetupInProgress() const {
+ return !IsSyncEnabledByUser() && WizardIsVisible();
+ }
+ bool WizardIsVisible() const { return wizard_.IsVisible(); }
+ void ShowLoginDialog();
+
+ // Pretty-printed strings for a given StatusSummary.
+ static std::wstring BuildSyncStatusSummaryText(
+ const browser_sync::SyncBackendHost::StatusSummary& summary);
+
+ // Returns true if the SyncBackendHost has told us it's ready to accept
+ // changes.
+ // TODO(timsteele): What happens if the bookmark model is loaded, a change
+ // takes place, and the backend isn't initialized yet?
+ bool sync_initialized() const { return backend_initialized_; }
+
+ bool UIShouldDepictAuthInProgress() const {
+ return is_auth_in_progress_;
+ }
+
+ // A timestamp marking the last time the service observed a transition from
+ // the SYNCING state to the READY state. Note that this does not reflect the
+ // last time we polled the server to see if there were any changes; the
+ // timestamp is only snapped when syncing takes place and we download or
+ // upload some bookmark entity.
+ const base::Time& last_synced_time() const { return last_synced_time_; }
+
+ // Returns a user-friendly string form of last synced time (in minutes).
+ std::wstring GetLastSyncedTimeString() const;
+
+ // Returns the authenticated username of the sync user, or empty if none
+ // exists. It will only exist if the authentication service provider (e.g
+ // GAIA) has confirmed the username is authentic.
+ virtual string16 GetAuthenticatedUsername() const;
+
+ const std::string& last_attempted_user_email() const {
+ return last_attempted_user_email_;
+ }
+
+ // The profile we are syncing for.
+ Profile* profile() { return profile_; }
+
+ // Adds/removes an observer. ProfileSyncService does not take ownership of
+ // the observer.
+ void AddObserver(Observer* observer);
+ void RemoveObserver(Observer* observer);
+
+ protected:
+ // Call this after any of the subsystems being synced (the bookmark
+ // model and the sync backend) finishes its initialization. When everything
+ // is ready, this function will bootstrap the subsystems so that they are
+ // initially in sync, and start forwarding changes between the two models.
+ void StartProcessingChangesIfReady();
+
+ // Various member accessors needed by unit tests.
+ browser_sync::SyncBackendHost* backend() { return backend_.get(); }
+
+ // Call this when normal operation detects that the bookmark model and the
+ // syncer model are inconsistent, or similar. The ProfileSyncService will
+ // try to avoid doing any work to avoid crashing or corrupting things
+ // further, and will report an error status if queried.
+ void SetUnrecoverableError();
+
+ // Returns whether processing changes is allowed. Check this before doing
+ // any model-modifying operations.
+ bool ShouldPushChanges();
+
+ // Starts up the backend sync components.
+ void StartUp();
+ // Shuts down the backend sync components.
+ // |sync_disabled| indicates if syncing is being disabled or not.
+ void Shutdown(bool sync_disabled);
+
+ // Tests need to override this.
+ virtual void InitializeBackend();
+
+ // Tests need this.
+ void set_model_associator(browser_sync::ModelAssociator* manager) {
+ model_associator_ = manager;
+ }
+
+ // We keep track of the last auth error observed so we can cover up the first
+ // "expected" auth failure from observers.
+ // TODO(timsteele): Same as expecting_first_run_auth_needed_event_. Remove
+ // this!
+ AuthErrorState last_auth_error_;
+
+ // Cache of the last name the client attempted to authenticate.
+ std::string last_attempted_user_email_;
+
+ private:
+ friend class browser_sync::ModelAssociator;
+ friend class ProfileSyncServiceTest;
+ friend class ProfileSyncServiceTestHarness;
+ friend class TestModelAssociator;
+ FRIEND_TEST(ProfileSyncServiceTest, UnrecoverableErrorSuspendsService);
+
+ enum MoveOrCreate {
+ MOVE,
+ CREATE,
+ };
+
+ // Initializes the various settings from the command line.
+ void InitSettings();
+
+ // Methods to register, load and remove preferences.
+ void RegisterPreferences();
+ void LoadPreferences();
+ void ClearPreferences();
+
+ // Treat the |index|th child of |parent| as a newly added node, and create a
+ // corresponding node in the sync domain using |trans|. All properties
+ // will be transferred to the new node. A node corresponding to |parent|
+ // must already exist and be associated for this call to succeed. Returns
+ // the ID of the just-created node, or if creation fails, kInvalidID.
+ int64 CreateSyncNode(const BookmarkNode* parent,
+ int index,
+ sync_api::WriteTransaction* trans);
+
+ // Create a bookmark node corresponding to |src| if one is not already
+ // associated with |src|. Returns the node that was created or updated.
+ const BookmarkNode* CreateOrUpdateBookmarkNode(
+ sync_api::BaseNode* src,
+ BookmarkModel* model);
+
+ // Creates a bookmark node under the given parent node from the given sync
+ // node. Returns the newly created node.
+ const BookmarkNode* CreateBookmarkNode(
+ sync_api::BaseNode* sync_node,
+ const BookmarkNode* parent,
+ int index) const;
+
+ // Sets the favicon of the given bookmark node from the given sync node.
+ // Returns whether the favicon was set in the bookmark node.
+ bool SetBookmarkFavicon(sync_api::BaseNode* sync_node,
+ const BookmarkNode* bookmark_node) const;
+
+ // Sets the favicon of the given sync node from the given bookmark node.
+ void SetSyncNodeFavicon(const BookmarkNode* bookmark_node,
+ sync_api::WriteNode* sync_node) const;
+
+ // Helper function to determine the appropriate insertion index of sync node
+ // |node| under the Bookmark model node |parent|, to make the positions
+ // match up between the two models. This presumes that the predecessor of the
+ // item (in the bookmark model) has already been moved into its appropriate
+ // position.
+ int CalculateBookmarkModelInsertionIndex(
+ const BookmarkNode* parent,
+ const sync_api::BaseNode* node) const;
+
+ // Helper function used to fix the position of a sync node so that it matches
+ // the position of a corresponding bookmark model node. |parent| and
+ // |index| identify the bookmark model position. |dst| is the node whose
+ // position is to be fixed. If |operation| is CREATE, treat |dst| as an
+ // uncreated node and set its position via InitByCreation(); otherwise,
+ // |dst| is treated as an existing node, and its position will be set via
+ // SetPosition(). |trans| is the transaction to which |dst| belongs. Returns
+ // false on failure.
+ bool PlaceSyncNode(MoveOrCreate operation,
+ const BookmarkNode* parent,
+ int index,
+ sync_api::WriteTransaction* trans,
+ sync_api::WriteNode* dst);
+
+ // Copy properties (but not position) from |src| to |dst|.
+ void UpdateSyncNodeProperties(const BookmarkNode* src,
+ sync_api::WriteNode* dst);
+
+ // Helper function to encode a bookmark's favicon into a PNG byte vector.
+ void EncodeFavicon(const BookmarkNode* src,
+ std::vector<unsigned char>* dst) const;
+
+ // Remove the sync node corresponding to |node|. It shouldn't have
+ // any children.
+ void RemoveOneSyncNode(sync_api::WriteTransaction* trans,
+ const BookmarkNode* node);
+
+ // Remove all the sync nodes associated with |node| and its children.
+ void RemoveSyncNodeHierarchy(const BookmarkNode* node);
+
+ // Whether the sync merge warning should be shown.
+ bool MergeAndSyncAcceptanceNeeded() const;
+
+ // Sets the last synced time to the current time.
+ void UpdateLastSyncedTime();
+
+ // The profile whose data we are synchronizing.
+ Profile* profile_;
+
+ // TODO(ncarter): Put this in a profile, once there is UI for it.
+ // This specifies where to find the sync server.
+ GURL sync_service_url_;
+
+ // Model assocation manager instance.
+ scoped_refptr<browser_sync::ModelAssociator> model_associator_;
+
+ // The last time we detected a successful transition from SYNCING state.
+ // Our backend notifies us whenever we should take a new snapshot.
+ base::Time last_synced_time_;
+
+ // Our asynchronous backend to communicate with sync components living on
+ // other threads.
+ scoped_ptr<browser_sync::SyncBackendHost> backend_;
+
+ // Whether the SyncBackendHost has been initialized.
+ bool backend_initialized_;
+
+ // Set to true when the user first enables sync, and we are waiting for
+ // syncapi to give us the green light on providing credentials for the first
+ // time. It is set back to false as soon as we get this message, and is
+ // false all other times so we don't have to persist this value as it will
+ // get initialized to false.
+ // TODO(timsteele): Remove this by way of starting the wizard when enabling
+ // sync *before* initializing the backend. syncapi will need to change, but
+ // it means we don't have to wait for the first AuthError; if we ever get
+ // one, it is actually an error and this bool isn't needed.
+ bool expecting_first_run_auth_needed_event_;
+
+ // Various pieces of UI query this value to determine if they should show
+ // an "Authenticating.." type of message. We are the only central place
+ // all auth attempts funnel through, so it makes sense to provide this.
+ // As its name suggests, this should NOT be used for anything other than UI.
+ bool is_auth_in_progress_;
+
+ // True only after all bootstrapping has succeeded: the bookmark model is
+ // loaded, the sync backend is initialized, and the two domains are
+ // consistent with one another.
+ bool ready_to_process_changes_;
+
+ // True if an unrecoverable error (e.g. violation of an assumed invariant)
+ // occurred during syncer operation. This value should be checked before
+ // doing any work that might corrupt things further.
+ bool unrecoverable_error_detected_;
+
+ SyncSetupWizard wizard_;
+
+ ObserverList<Observer> observers_;
+
+ DISALLOW_COPY_AND_ASSIGN(ProfileSyncService);
+};
+
+#endif // CHROME_BROWSER_SYNC_PROFILE_SYNC_SERVICE_H_
+
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/sync/profile_sync_service_unittest.cc b/chrome/browser/sync/profile_sync_service_unittest.cc
new file mode 100644
index 0000000..a4d1421
--- /dev/null
+++ b/chrome/browser/sync/profile_sync_service_unittest.cc
@@ -0,0 +1,1273 @@
+// Copyright (c) 2006-2008 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#include <stack>
+#include <vector>
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+#include "base/command_line.h"
+#include "base/string_util.h"
+#include "base/string16.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/sync/engine/syncapi.h"
+#include "chrome/browser/sync/glue/model_associator.h"
+#include "chrome/browser/sync/profile_sync_service.h"
+#include "chrome/common/pref_service.h"
+#include "chrome/test/testing_profile.h"
+
+using std::vector;
+using browser_sync::ModelAssociator;
+using browser_sync::SyncBackendHost;
+
+class TestModelAssociator : public ModelAssociator {
+ public:
+ explicit TestModelAssociator(ProfileSyncService* service)
+ : ModelAssociator(service) {
+ }
+
+ virtual bool GetSyncIdForTaggedNode(const string16& tag, int64* sync_id) {
+ sync_api::WriteTransaction trans(
+ sync_service()->backend()->GetUserShareHandle());
+ sync_api::ReadNode root(&trans);
+ root.InitByRootLookup();
+
+ // First, try to find a node with the title among the root's children.
+ // This will be the case if we are testing model persistence, and
+ // are reloading a sync repository created earlier in the test.
+ for (int64 id = root.GetFirstChildId(); id != sync_api::kInvalidId; /***/) {
+ sync_api::ReadNode child(&trans);
+ if (!child.InitByIdLookup(id)) {
+ NOTREACHED();
+ break;
+ }
+ if (tag == child.GetTitle()) {
+ *sync_id = id;
+ return true;
+ }
+ id = child.GetSuccessorId();
+ }
+
+ sync_api::WriteNode node(&trans);
+ if (!node.InitByCreation(root, NULL))
+ return false;
+ node.SetIsFolder(true);
+ node.SetTitle(tag.c_str());
+ node.SetExternalId(0);
+ *sync_id = node.GetId();
+ return true;
+ }
+};
+
+class TestProfileSyncService : public ProfileSyncService {
+ public:
+ explicit TestProfileSyncService(Profile* profile)
+ : ProfileSyncService(profile) {
+ PrefService* pref_service = profile->GetPrefs();
+ if (pref_service->IsPrefRegistered(prefs::kSyncUserName))
+ return;
+ pref_service->RegisterStringPref(prefs::kSyncUserName, string16());
+ pref_service->RegisterStringPref(prefs::kSyncLastSyncedTime, string16());
+ pref_service->RegisterBooleanPref(prefs::kSyncHasSetupCompleted, true);
+ }
+ virtual ~TestProfileSyncService() {
+ }
+
+ virtual void InitializeBackend() {
+ set_model_associator(new TestModelAssociator(this));
+ backend()->InitializeForTestMode(L"testuser");
+ // The SyncBackend posts a task to the current loop when initialization
+ // completes.
+ MessageLoop::current()->Run();
+ // Initialization is synchronous for test mode, so we should be good to go.
+ DCHECK(sync_initialized());
+ }
+
+ virtual void OnBackendInitialized() {
+ ProfileSyncService::OnBackendInitialized();
+ MessageLoop::current()->Quit();
+ }
+
+ virtual bool MergeAndSyncAcceptanceNeeded() {
+ // Never show the dialog.
+ return false;
+ }
+};
+
+// FakeServerChange constructs a list of sync_api::ChangeRecords while modifying
+// the sync model, and can pass the ChangeRecord list to a
+// sync_api::SyncObserver (i.e., the ProfileSyncService) to test the client
+// change-application behavior.
+// Tests using FakeServerChange should be careful to avoid back-references,
+// since FakeServerChange will send the edits in the order specified.
+class FakeServerChange {
+ public:
+ explicit FakeServerChange(sync_api::WriteTransaction* trans) : trans_(trans) {
+ }
+
+ // Pretend that the server told the syncer to add a bookmark object.
+ int64 Add(const string16& title,
+ const string16& url,
+ bool is_folder,
+ int64 parent_id,
+ int64 predecessor_id) {
+ sync_api::ReadNode parent(trans_);
+ EXPECT_TRUE(parent.InitByIdLookup(parent_id));
+ sync_api::WriteNode node(trans_);
+ if (predecessor_id == 0) {
+ EXPECT_TRUE(node.InitByCreation(parent, NULL));
+ } else {
+ sync_api::ReadNode predecessor(trans_);
+ EXPECT_TRUE(predecessor.InitByIdLookup(predecessor_id));
+ EXPECT_EQ(predecessor.GetParentId(), parent.GetId());
+ EXPECT_TRUE(node.InitByCreation(parent, &predecessor));
+ }
+ EXPECT_EQ(node.GetPredecessorId(), predecessor_id);
+ EXPECT_EQ(node.GetParentId(), parent_id);
+ node.SetIsFolder(is_folder);
+ node.SetTitle(title.c_str());
+ if (!is_folder) {
+ GURL gurl(url);
+ node.SetURL(url.c_str());
+ }
+ sync_api::SyncManager::ChangeRecord record;
+ record.action = sync_api::SyncManager::ChangeRecord::ACTION_ADD;
+ record.id = node.GetId();
+ changes_.push_back(record);
+ return node.GetId();
+ }
+
+ // Add a bookmark folder.
+ int64 AddFolder(const string16& title,
+ int64 parent_id,
+ int64 predecessor_id) {
+ return Add(title, string16(), true, parent_id, predecessor_id);
+ }
+
+ // Add a bookmark.
+ int64 AddURL(const string16& title,
+ const string16& url,
+ int64 parent_id,
+ int64 predecessor_id) {
+ return Add(title, url, false, parent_id, predecessor_id);
+ }
+
+ // Pretend that the server told the syncer to delete an object.
+ void Delete(int64 id) {
+ {
+ // Delete the sync node.
+ sync_api::WriteNode node(trans_);
+ EXPECT_TRUE(node.InitByIdLookup(id));
+ EXPECT_FALSE(node.GetFirstChildId());
+ node.Remove();
+ }
+ {
+ // Verify the deletion.
+ sync_api::ReadNode node(trans_);
+ EXPECT_FALSE(node.InitByIdLookup(id));
+ }
+
+ sync_api::SyncManager::ChangeRecord record;
+ record.action = sync_api::SyncManager::ChangeRecord::ACTION_DELETE;
+ record.id = id;
+ // Deletions are always first in the changelist, but we can't actually do
+ // WriteNode::Remove() on the node until its children are moved. So, as
+ // a practical matter, users of FakeServerChange must move or delete
+ // children before parents. Thus, we must insert the deletion record
+ // at the front of the vector.
+ changes_.insert(changes_.begin(), record);
+ }
+
+ // Set a new title value, and return the old value.
+ string16 ModifyTitle(int64 id, const string16& new_title) {
+ sync_api::WriteNode node(trans_);
+ EXPECT_TRUE(node.InitByIdLookup(id));
+ string16 old_title = node.GetTitle();
+ node.SetTitle(new_title.c_str());
+ SetModified(id);
+ return old_title;
+ }
+
+ // Set a new URL value, and return the old value.
+ // TODO(ncarter): Determine if URL modifications are even legal.
+ string16 ModifyURL(int64 id, const string16& new_url) {
+ sync_api::WriteNode node(trans_);
+ EXPECT_TRUE(node.InitByIdLookup(id));
+ EXPECT_FALSE(node.GetIsFolder());
+ string16 old_url = node.GetURL();
+ node.SetURL(new_url.c_str());
+ SetModified(id);
+ return old_url;
+ }
+
+ // Set a new parent and predecessor value. Return the old parent id.
+ // We could return the old predecessor id, but it turns out not to be
+ // very useful for assertions.
+ int64 ModifyPosition(int64 id, int64 parent_id, int64 predecessor_id) {
+ sync_api::ReadNode parent(trans_);
+ EXPECT_TRUE(parent.InitByIdLookup(parent_id));
+ sync_api::WriteNode node(trans_);
+ EXPECT_TRUE(node.InitByIdLookup(id));
+ int64 old_parent_id = node.GetParentId();
+ if (predecessor_id == 0) {
+ EXPECT_TRUE(node.SetPosition(parent, NULL));
+ } else {
+ sync_api::ReadNode predecessor(trans_);
+ EXPECT_TRUE(predecessor.InitByIdLookup(predecessor_id));
+ EXPECT_EQ(predecessor.GetParentId(), parent.GetId());
+ EXPECT_TRUE(node.SetPosition(parent, &predecessor));
+ }
+ SetModified(id);
+ return old_parent_id;
+ }
+
+ // Pass the fake change list to |service|.
+ void ApplyPendingChanges(ProfileSyncService* service) {
+ service->ApplyModelChanges(trans_, changes_.size() ? &changes_[0] : NULL,
+ changes_.size());
+ }
+
+ const vector<sync_api::SyncManager::ChangeRecord>& changes() {
+ return changes_;
+ }
+
+ private:
+ // Helper function to push an ACTION_UPDATE record onto the back
+ // of the changelist.
+ void SetModified(int64 id) {
+ // Coalesce multi-property edits.
+ if (changes_.size() > 0 && changes_.back().id == id &&
+ changes_.back().action ==
+ sync_api::SyncManager::ChangeRecord::ACTION_UPDATE)
+ return;
+ sync_api::SyncManager::ChangeRecord record;
+ record.action = sync_api::SyncManager::ChangeRecord::ACTION_UPDATE;
+ record.id = id;
+ changes_.push_back(record);
+ }
+
+ // The transaction on which everything happens.
+ sync_api::WriteTransaction *trans_;
+
+ // The change list we construct.
+ vector<sync_api::SyncManager::ChangeRecord> changes_;
+};
+
+class ProfileSyncServiceTest : public testing::Test {
+ protected:
+ enum LoadOption { LOAD_FROM_STORAGE, DELETE_EXISTING_STORAGE };
+ enum SaveOption { SAVE_TO_STORAGE, DONT_SAVE_TO_STORAGE };
+ ProfileSyncServiceTest() : model_(NULL) {
+ profile_.reset(new TestingProfile());
+ profile_->set_has_history_service(true);
+ }
+ virtual ~ProfileSyncServiceTest() {
+ // Kill the service before the profile.
+ service_.reset();
+ profile_.reset();
+ }
+
+ ModelAssociator* associator() {
+ DCHECK(service_.get());
+ return service_->model_associator_;
+ }
+
+ void StartSyncService() {
+ if (!service_.get()) {
+ service_.reset(new TestProfileSyncService(profile_.get()));
+ service_->Initialize();
+ }
+ // The service may have already started sync automatically if it's already
+ // enabled by user once.
+ if (!service_->IsSyncEnabledByUser())
+ service_->EnableForUser();
+ }
+ void StopSyncService(SaveOption save) {
+ if (save == DONT_SAVE_TO_STORAGE)
+ service_->DisableForUser();
+ service_.reset();
+ }
+
+ // Load (or re-load) the bookmark model. |load| controls use of the
+ // bookmarks file on disk. |save| controls whether the newly loaded
+ // bookmark model will write out a bookmark file as it goes.
+ void LoadBookmarkModel(LoadOption load, SaveOption save) {
+ bool delete_bookmarks = load == DELETE_EXISTING_STORAGE;
+ profile_->CreateBookmarkModel(delete_bookmarks);
+ model_ = profile_->GetBookmarkModel();
+ // Wait for the bookmarks model to load.
+ profile_->BlockUntilBookmarkModelLoaded();
+ // This noticeably speeds up the unit tests that request it.
+ if (save == DONT_SAVE_TO_STORAGE)
+ model_->ClearStore();
+ }
+
+ void ExpectSyncerNodeMatching(sync_api::BaseTransaction* trans,
+ const BookmarkNode* bnode) {
+ sync_api::ReadNode gnode(trans);
+ EXPECT_TRUE(associator()->InitSyncNodeFromBookmarkId(bnode->id(), &gnode));
+ // Non-root node titles and parents must match.
+ if (bnode != model_->GetBookmarkBarNode() &&
+ bnode != model_->other_node()) {
+ EXPECT_EQ(bnode->GetTitle(), gnode.GetTitle());
+ EXPECT_EQ(associator()->GetBookmarkNodeFromSyncId(gnode.GetParentId()),
+ bnode->GetParent());
+ }
+ EXPECT_EQ(bnode->is_folder(), gnode.GetIsFolder());
+ if (bnode->is_url())
+ EXPECT_EQ(bnode->GetURL(), GURL(gnode.GetURL()));
+
+ // Check for position matches.
+ int browser_index = bnode->GetParent()->IndexOfChild(bnode);
+ if (browser_index == 0) {
+ EXPECT_EQ(gnode.GetPredecessorId(), 0);
+ } else {
+ const BookmarkNode* bprev =
+ bnode->GetParent()->GetChild(browser_index - 1);
+ sync_api::ReadNode gprev(trans);
+ ASSERT_TRUE(associator()->InitSyncNodeFromBookmarkId(bprev->id(),
+ &gprev));
+ EXPECT_EQ(gnode.GetPredecessorId(), gprev.GetId());
+ EXPECT_EQ(gnode.GetParentId(), gprev.GetParentId());
+ }
+ if (browser_index == bnode->GetParent()->GetChildCount() - 1) {
+ EXPECT_EQ(gnode.GetSuccessorId(), 0);
+ } else {
+ const BookmarkNode* bnext =
+ bnode->GetParent()->GetChild(browser_index + 1);
+ sync_api::ReadNode gnext(trans);
+ ASSERT_TRUE(associator()->InitSyncNodeFromBookmarkId(bnext->id(),
+ &gnext));
+ EXPECT_EQ(gnode.GetSuccessorId(), gnext.GetId());
+ EXPECT_EQ(gnode.GetParentId(), gnext.GetParentId());
+ }
+ if (bnode->GetChildCount()) {
+ EXPECT_TRUE(gnode.GetFirstChildId());
+ }
+ }
+
+ void ExpectSyncerNodeMatching(const BookmarkNode* bnode) {
+ sync_api::ReadTransaction trans(service_->backend_->GetUserShareHandle());
+ ExpectSyncerNodeMatching(&trans, bnode);
+ }
+
+ void ExpectBrowserNodeMatching(sync_api::BaseTransaction* trans,
+ int64 sync_id) {
+ EXPECT_TRUE(sync_id);
+ const BookmarkNode* bnode =
+ associator()->GetBookmarkNodeFromSyncId(sync_id);
+ ASSERT_TRUE(bnode);
+ int64 id = associator()->GetSyncIdFromBookmarkId(bnode->id());
+ EXPECT_EQ(id, sync_id);
+ ExpectSyncerNodeMatching(trans, bnode);
+ }
+
+ void ExpectBrowserNodeUnknown(int64 sync_id) {
+ EXPECT_FALSE(associator()->GetBookmarkNodeFromSyncId(sync_id));
+ }
+
+ void ExpectBrowserNodeKnown(int64 sync_id) {
+ EXPECT_TRUE(associator()->GetBookmarkNodeFromSyncId(sync_id));
+ }
+
+ void ExpectSyncerNodeKnown(const BookmarkNode* node) {
+ int64 sync_id = associator()->GetSyncIdFromBookmarkId(node->id());
+ EXPECT_NE(sync_id, sync_api::kInvalidId);
+ }
+
+ void ExpectSyncerNodeUnknown(const BookmarkNode* node) {
+ int64 sync_id = associator()->GetSyncIdFromBookmarkId(node->id());
+ EXPECT_EQ(sync_id, sync_api::kInvalidId);
+ }
+
+ void ExpectBrowserNodeTitle(int64 sync_id, const string16& title) {
+ const BookmarkNode* bnode =
+ associator()->GetBookmarkNodeFromSyncId(sync_id);
+ ASSERT_TRUE(bnode);
+ EXPECT_EQ(bnode->GetTitle(), title);
+ }
+
+ void ExpectBrowserNodeURL(int64 sync_id, const string16& url) {
+ const BookmarkNode* bnode =
+ associator()->GetBookmarkNodeFromSyncId(sync_id);
+ ASSERT_TRUE(bnode);
+ GURL url2(url);
+ EXPECT_EQ(url2, bnode->GetURL());
+ }
+
+ void ExpectBrowserNodeParent(int64 sync_id, int64 parent_sync_id) {
+ const BookmarkNode* node = associator()->GetBookmarkNodeFromSyncId(sync_id);
+ ASSERT_TRUE(node);
+ const BookmarkNode* parent =
+ associator()->GetBookmarkNodeFromSyncId(parent_sync_id);
+ EXPECT_TRUE(parent);
+ EXPECT_EQ(node->GetParent(), parent);
+ }
+
+ void ExpectModelMatch(sync_api::BaseTransaction* trans) {
+ const BookmarkNode* root = model_->root_node();
+ EXPECT_EQ(root->IndexOfChild(model_->GetBookmarkBarNode()), 0);
+ EXPECT_EQ(root->IndexOfChild(model_->other_node()), 1);
+
+ std::stack<int64> stack;
+ stack.push(bookmark_bar_id());
+ while (!stack.empty()) {
+ int64 id = stack.top();
+ stack.pop();
+ if (!id) continue;
+
+ ExpectBrowserNodeMatching(trans, id);
+
+ sync_api::ReadNode gnode(trans);
+ ASSERT_TRUE(gnode.InitByIdLookup(id));
+ stack.push(gnode.GetFirstChildId());
+ stack.push(gnode.GetSuccessorId());
+ }
+ }
+
+ void ExpectModelMatch() {
+ sync_api::ReadTransaction trans(service_->backend_->GetUserShareHandle());
+ ExpectModelMatch(&trans);
+ }
+
+ int64 other_bookmarks_id() {
+ return associator()->GetSyncIdFromBookmarkId(model_->other_node()->id());
+ }
+
+ int64 bookmark_bar_id() {
+ return associator()->GetSyncIdFromBookmarkId(
+ model_->GetBookmarkBarNode()->id());
+ }
+
+ SyncBackendHost* backend() { return service_->backend_.get(); }
+
+ // This serves as the "UI loop" on which the ProfileSyncService lives and
+ // operates. It is needed because the SyncBackend can post tasks back to
+ // the service, meaning it can't be null. It doesn't have to be running,
+ // though -- OnInitializationCompleted is the only example (so far) in this
+ // test where we need to Run the loop to swallow a task and then quit, to
+ // avoid leaking the ProfileSyncService (the PostTask will retain the callee
+ // and caller until the task is run).
+ MessageLoop message_loop_;
+
+ scoped_ptr<ProfileSyncService> service_;
+ scoped_ptr<TestingProfile> profile_;
+ BookmarkModel* model_;
+};
+
+TEST_F(ProfileSyncServiceTest, InitialState) {
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, DONT_SAVE_TO_STORAGE);
+ StartSyncService();
+
+ EXPECT_TRUE(other_bookmarks_id());
+ EXPECT_TRUE(bookmark_bar_id());
+
+ ExpectModelMatch();
+}
+
+TEST_F(ProfileSyncServiceTest, BookmarkModelOperations) {
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, DONT_SAVE_TO_STORAGE);
+ StartSyncService();
+
+ // Test addition.
+ const BookmarkNode* folder =
+ model_->AddGroup(model_->other_node(), 0, L"foobar");
+ ExpectSyncerNodeMatching(folder);
+ ExpectModelMatch();
+ const BookmarkNode* folder2 = model_->AddGroup(folder, 0, L"nested");
+ ExpectSyncerNodeMatching(folder2);
+ ExpectModelMatch();
+ const BookmarkNode* url1 = model_->AddURL(
+ folder, 0, L"Internets #1 Pies Site", GURL(L"http://www.easypie.com/"));
+ ExpectSyncerNodeMatching(url1);
+ ExpectModelMatch();
+ const BookmarkNode* url2 = model_->AddURL(
+ folder, 1, L"Airplanes", GURL(L"http://www.easyjet.com/"));
+ ExpectSyncerNodeMatching(url2);
+ ExpectModelMatch();
+
+ // Test modification.
+ model_->SetTitle(url2, L"EasyJet");
+ ExpectModelMatch();
+ model_->Move(url1, folder2, 0);
+ ExpectModelMatch();
+ model_->Move(folder2, model_->GetBookmarkBarNode(), 0);
+ ExpectModelMatch();
+ model_->SetTitle(folder2, L"Not Nested");
+ ExpectModelMatch();
+ model_->Move(folder, folder2, 0);
+ ExpectModelMatch();
+ model_->SetTitle(folder, L"who's nested now?");
+ ExpectModelMatch();
+
+ // Test deletion.
+ // Delete a single item.
+ model_->Remove(url2->GetParent(), url2->GetParent()->IndexOfChild(url2));
+ ExpectModelMatch();
+ // Delete an item with several children.
+ model_->Remove(folder2->GetParent(),
+ folder2->GetParent()->IndexOfChild(folder2));
+ ExpectModelMatch();
+}
+
+TEST_F(ProfileSyncServiceTest, ServerChangeProcessing) {
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, DONT_SAVE_TO_STORAGE);
+ StartSyncService();
+
+ sync_api::WriteTransaction trans(backend()->GetUserShareHandle());
+
+ FakeServerChange adds(&trans);
+ int64 f1 = adds.AddFolder(L"Server Folder B", bookmark_bar_id(), 0);
+ int64 f2 = adds.AddFolder(L"Server Folder A", bookmark_bar_id(), f1);
+ int64 u1 = adds.AddURL(L"Some old site", L"ftp://nifty.andrew.cmu.edu/",
+ bookmark_bar_id(), f2);
+ int64 u2 = adds.AddURL(L"Nifty", L"ftp://nifty.andrew.cmu.edu/", f1, 0);
+ // u3 is a duplicate URL
+ int64 u3 = adds.AddURL(L"Nifty2", L"ftp://nifty.andrew.cmu.edu/", f1, u2);
+ // u4 is a duplicate title, different URL.
+ int64 u4 = adds.AddURL(L"Some old site", L"http://slog.thestranger.com/",
+ bookmark_bar_id(), u1);
+ // u5 tests an empty-string title.
+ string16 javascript_url(L"javascript:(function(){var w=window.open(" \
+ L"'about:blank','gnotesWin','location=0,menubar=0," \
+ L"scrollbars=0,status=0,toolbar=0,width=300," \
+ L"height=300,resizable');});");
+ int64 u5 = adds.AddURL(L"", javascript_url, other_bookmarks_id(), 0);
+
+ vector<sync_api::SyncManager::ChangeRecord>::const_iterator it;
+ // The bookmark model shouldn't yet have seen any of the nodes of |adds|.
+ for (it = adds.changes().begin(); it != adds.changes().end(); ++it)
+ ExpectBrowserNodeUnknown(it->id);
+
+ adds.ApplyPendingChanges(service_.get());
+
+ // Make sure the bookmark model received all of the nodes in |adds|.
+ for (it = adds.changes().begin(); it != adds.changes().end(); ++it)
+ ExpectBrowserNodeMatching(&trans, it->id);
+ ExpectModelMatch(&trans);
+
+ // Part two: test modifications.
+ FakeServerChange mods(&trans);
+ // Mess with u2, and move it into empty folder f2
+ // TODO(ncarter): Determine if we allow ModifyURL ops or not.
+ /* string16 u2_old_url = mods.ModifyURL(u2, L"http://www.google.com"); */
+ string16 u2_old_title = mods.ModifyTitle(u2, L"The Google");
+ int64 u2_old_parent = mods.ModifyPosition(u2, f2, 0);
+
+ // Now move f1 after u2.
+ string16 f1_old_title = mods.ModifyTitle(f1, L"Server Folder C");
+ int64 f1_old_parent = mods.ModifyPosition(f1, f2, u2);
+
+ // Then add u3 after f1.
+ int64 u3_old_parent = mods.ModifyPosition(u3, f2, f1);
+
+ // Test that the property changes have not yet taken effect.
+ ExpectBrowserNodeTitle(u2, u2_old_title);
+ /* ExpectBrowserNodeURL(u2, u2_old_url); */
+ ExpectBrowserNodeParent(u2, u2_old_parent);
+
+ ExpectBrowserNodeTitle(f1, f1_old_title);
+ ExpectBrowserNodeParent(f1, f1_old_parent);
+
+ ExpectBrowserNodeParent(u3, u3_old_parent);
+
+ // Apply the changes.
+ mods.ApplyPendingChanges(service_.get());
+
+ // Check for successful application.
+ for (it = mods.changes().begin(); it != mods.changes().end(); ++it)
+ ExpectBrowserNodeMatching(&trans, it->id);
+ ExpectModelMatch(&trans);
+
+ // Part 3: Test URL deletion.
+ FakeServerChange dels(&trans);
+ dels.Delete(u2);
+ dels.Delete(u3);
+
+ ExpectBrowserNodeKnown(u2);
+ ExpectBrowserNodeKnown(u3);
+
+ dels.ApplyPendingChanges(service_.get());
+
+ ExpectBrowserNodeUnknown(u2);
+ ExpectBrowserNodeUnknown(u3);
+ ExpectModelMatch(&trans);
+}
+
+// Tests a specific case in ApplyModelChanges where we move the
+// children out from under a parent, and then delete the parent
+// in the same changelist. The delete shows up first in the changelist,
+// requiring the children to be moved to a temporary location.
+TEST_F(ProfileSyncServiceTest, ServerChangeRequiringFosterParent) {
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, DONT_SAVE_TO_STORAGE);
+ StartSyncService();
+
+ sync_api::WriteTransaction trans(backend()->GetUserShareHandle());
+
+ // Stress the immediate children of other_node because that's where
+ // ApplyModelChanges puts a temporary foster parent node.
+ string16 url(L"http://dev.chromium.org/");
+ FakeServerChange adds(&trans);
+ int64 f0 = other_bookmarks_id(); // + other_node
+ int64 f1 = adds.AddFolder(L"f1", f0, 0); // + f1
+ int64 f2 = adds.AddFolder(L"f2", f1, 0); // + f2
+ int64 u3 = adds.AddURL( L"u3", url, f2, 0); // + u3
+ int64 u4 = adds.AddURL( L"u4", url, f2, u3); // + u4
+ int64 u5 = adds.AddURL( L"u5", url, f1, f2); // + u5
+ int64 f6 = adds.AddFolder(L"f6", f1, u5); // + f6
+ int64 u7 = adds.AddURL( L"u7", url, f0, f1); // + u7
+
+ vector<sync_api::SyncManager::ChangeRecord>::const_iterator it;
+ // The bookmark model shouldn't yet have seen any of the nodes of |adds|.
+ for (it = adds.changes().begin(); it != adds.changes().end(); ++it)
+ ExpectBrowserNodeUnknown(it->id);
+
+ adds.ApplyPendingChanges(service_.get());
+
+ // Make sure the bookmark model received all of the nodes in |adds|.
+ for (it = adds.changes().begin(); it != adds.changes().end(); ++it)
+ ExpectBrowserNodeMatching(&trans, it->id);
+ ExpectModelMatch(&trans);
+
+ // We have to do the moves before the deletions, but FakeServerChange will
+ // put the deletion at the front of the changelist.
+ FakeServerChange ops(&trans);
+ ops.ModifyPosition(f6, other_bookmarks_id(), 0);
+ ops.ModifyPosition(u3, other_bookmarks_id(), f1); // Prev == f1 is OK here.
+ ops.ModifyPosition(f2, other_bookmarks_id(), u7);
+ ops.ModifyPosition(u7, f2, 0);
+ ops.ModifyPosition(u4, other_bookmarks_id(), f2);
+ ops.ModifyPosition(u5, f6, 0);
+ ops.Delete(f1);
+
+ ops.ApplyPendingChanges(service_.get());
+
+ ExpectModelMatch(&trans);
+}
+
+// Simulate a server change record containing a valid but non-canonical URL.
+TEST_F(ProfileSyncServiceTest, ServerChangeWithNonCanonicalURL) {
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, SAVE_TO_STORAGE);
+ StartSyncService();
+
+ {
+ sync_api::WriteTransaction trans(backend()->GetUserShareHandle());
+
+ FakeServerChange adds(&trans);
+ std::string url("http://dev.chromium.org");
+ EXPECT_NE(GURL(url).spec(), url);
+ int64 u1 = adds.AddURL(L"u1", UTF8ToWide(url), other_bookmarks_id(), 0);
+
+ adds.ApplyPendingChanges(service_.get());
+
+ EXPECT_TRUE(model_->other_node()->GetChildCount() == 1);
+ ExpectModelMatch(&trans);
+ }
+
+ // Now reboot the sync service, forcing a merge step.
+ StopSyncService(SAVE_TO_STORAGE);
+ LoadBookmarkModel(LOAD_FROM_STORAGE, SAVE_TO_STORAGE);
+ StartSyncService();
+
+ // There should still be just the one bookmark.
+ EXPECT_TRUE(model_->other_node()->GetChildCount() == 1);
+ ExpectModelMatch();
+}
+
+// Simulate a server change record containing an invalid URL (per GURL).
+// TODO(ncarter): Disabled due to crashes. Fix bug 1677563.
+TEST_F(ProfileSyncServiceTest, DISABLED_ServerChangeWithInvalidURL) {
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, SAVE_TO_STORAGE);
+ StartSyncService();
+
+ int child_count = 0;
+ {
+ sync_api::WriteTransaction trans(backend()->GetUserShareHandle());
+
+ FakeServerChange adds(&trans);
+ EXPECT_FALSE(GURL("x").is_valid());
+ int64 u1 = adds.AddURL(L"u1", L"x", other_bookmarks_id(), 0);
+
+ adds.ApplyPendingChanges(service_.get());
+
+ // We're lenient about what should happen -- the model could wind up with
+ // the node or without it; but things should be consistent, and we
+ // shouldn't crash.
+ child_count = model_->other_node()->GetChildCount();
+ EXPECT_TRUE(child_count == 0 || child_count == 1);
+ ExpectModelMatch(&trans);
+ }
+
+ // Now reboot the sync service, forcing a merge step.
+ StopSyncService(SAVE_TO_STORAGE);
+ LoadBookmarkModel(LOAD_FROM_STORAGE, SAVE_TO_STORAGE);
+ StartSyncService();
+
+ // Things ought not to have changed.
+ EXPECT_EQ(model_->other_node()->GetChildCount(), child_count);
+ ExpectModelMatch();
+}
+
+// Test strings that might pose a problem if the titles ever became used as
+// file names in the sync backend.
+TEST_F(ProfileSyncServiceTest, CornerCaseNames) {
+ // TODO(ncarter): Bug 1570238 explains the failure of this test.
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, DONT_SAVE_TO_STORAGE);
+ StartSyncService();
+
+ char16* names[] = {
+ // The empty string.
+ L"",
+ // Illegal Windows filenames.
+ L"CON", L"PRN", L"AUX", L"NUL", L"COM1", L"COM2", L"COM3", L"COM4",
+ L"COM5", L"COM6", L"COM7", L"COM8", L"COM9", L"LPT1", L"LPT2", L"LPT3",
+ L"LPT4", L"LPT5", L"LPT6", L"LPT7", L"LPT8", L"LPT9",
+ // Current/parent directory markers.
+ L".", L"..", L"...",
+ // Files created automatically by the Windows shell.
+ L"Thumbs.db", L".DS_Store",
+ // Names including Win32-illegal characters, and path separators.
+ L"foo/bar", L"foo\\bar", L"foo?bar", L"foo:bar", L"foo|bar", L"foo\"bar",
+ L"foo'bar", L"foo<bar", L"foo>bar", L"foo%bar", L"foo*bar", L"foo]bar",
+ L"foo[bar",
+ };
+ // Create both folders and bookmarks using each name.
+ GURL url("http://www.doublemint.com");
+ for (int i = 0; i < arraysize(names); ++i) {
+ model_->AddGroup(model_->other_node(), 0, names[i]);
+ model_->AddURL(model_->other_node(), 0, names[i], url);
+ }
+
+ // Verify that the browser model matches the sync model.
+ EXPECT_EQ(model_->other_node()->GetChildCount(), 2*arraysize(names));
+ ExpectModelMatch();
+}
+
+// Stress the internal representation of position by sparse numbers. We want
+// to repeatedly bisect the range of available positions, to force the
+// syncer code to renumber its ranges. Pick a number big enough so that it
+// would exhaust 32bits of room between items a couple of times.
+TEST_F(ProfileSyncServiceTest, RepeatedMiddleInsertion) {
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, DONT_SAVE_TO_STORAGE);
+ StartSyncService();
+
+ static const int kTimesToInsert = 256;
+
+ // Create two book-end nodes to insert between.
+ model_->AddGroup(model_->other_node(), 0, L"Alpha");
+ model_->AddGroup(model_->other_node(), 1, L"Omega");
+ int count = 2;
+
+ // Test insertion in first half of range by repeatedly inserting in second
+ // position.
+ for (int i = 0; i < kTimesToInsert; ++i) {
+ string16 title = string16(L"Pre-insertion ") + IntToWString(i);
+ model_->AddGroup(model_->other_node(), 1, title);
+ count++;
+ }
+
+ // Test insertion in second half of range by repeatedly inserting in
+ // second-to-last position.
+ for (int i = 0; i < kTimesToInsert; ++i) {
+ string16 title = string16(L"Post-insertion ") + IntToWString(i);
+ model_->AddGroup(model_->other_node(), count - 1, title);
+ count++;
+ }
+
+ // Verify that the browser model matches the sync model.
+ EXPECT_EQ(model_->other_node()->GetChildCount(), count);
+ ExpectModelMatch();
+}
+
+// Introduce a consistency violation into the model, and see that it
+// puts itself into a lame, error state.
+TEST_F(ProfileSyncServiceTest, UnrecoverableErrorSuspendsService) {
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, DONT_SAVE_TO_STORAGE);
+ StartSyncService();
+
+ // Synchronization should be up and running at this point.
+ EXPECT_TRUE(service_->ShouldPushChanges());
+
+ // Add a node which will be the target of the consistency violation.
+ const BookmarkNode* node =
+ model_->AddGroup(model_->other_node(), 0, L"node");
+ ExpectSyncerNodeMatching(node);
+
+ // Now destroy the syncer node as if we were the ProfileSyncService without
+ // updating the ProfileSyncService state. This should introduce
+ // inconsistency between the two models.
+ {
+ sync_api::WriteTransaction trans(service_->backend_->GetUserShareHandle());
+ sync_api::WriteNode sync_node(&trans);
+ EXPECT_TRUE(associator()->InitSyncNodeFromBookmarkId(node->id(),
+ &sync_node));
+ sync_node.Remove();
+ }
+ // The models don't match at this point, but the ProfileSyncService
+ // doesn't know it yet.
+ ExpectSyncerNodeKnown(node);
+ EXPECT_TRUE(service_->ShouldPushChanges());
+
+ // Add a child to the inconsistent node. This should cause detection of the
+ // problem.
+ const BookmarkNode* nested = model_->AddGroup(node, 0, L"nested");
+ EXPECT_FALSE(service_->ShouldPushChanges());
+ ExpectSyncerNodeUnknown(nested);
+
+ // Try to add a node under a totally different parent. This should also
+ // fail -- the ProfileSyncService should stop processing changes after
+ // encountering a consistency violation.
+ const BookmarkNode* unrelated = model_->AddGroup(
+ model_->GetBookmarkBarNode(), 0, L"unrelated");
+ EXPECT_FALSE(service_->ShouldPushChanges());
+ ExpectSyncerNodeUnknown(unrelated);
+
+ // TODO(ncarter): We ought to test the ProfileSyncService state machine
+ // directly here once that's formalized and exposed.
+}
+
+struct TestData {
+ const char16* title;
+ const char16* url;
+};
+
+// TODO(ncarter): Integrate the existing TestNode/PopulateNodeFromString code
+// in the bookmark model unittest, to make it simpler to set up test data
+// here (and reduce the amount of duplication among tests), and to reduce the
+// duplication.
+class ProfileSyncServiceTestWithData : public ProfileSyncServiceTest {
+ protected:
+ // Populates or compares children of the given bookmark node from/with the
+ // given test data array with the given size.
+ void PopulateFromTestData(const BookmarkNode* node,
+ const TestData* data,
+ int size);
+ void CompareWithTestData(const BookmarkNode* node,
+ const TestData* data,
+ int size);
+
+ void ExpectBookmarkModelMatchesTestData();
+ void WriteTestDataToBookmarkModel();
+};
+
+namespace {
+
+// Constants for bookmark model that looks like:
+// |-- Bookmark bar
+// | |-- u2, http://www.u2.com/
+// | |-- f1
+// | | |-- f1u4, http://www.f1u4.com/
+// | | |-- f1u2, http://www.f1u2.com/
+// | | |-- f1u3, http://www.f1u3.com/
+// | | +-- f1u1, http://www.f1u1.com/
+// | |-- u1, http://www.u1.com/
+// | +-- f2
+// | |-- f2u2, http://www.f2u2.com/
+// | |-- f2u4, http://www.f2u4.com/
+// | |-- f2u3, http://www.f2u3.com/
+// | +-- f2u1, http://www.f2u1.com/
+// +-- Other bookmarks
+// |-- f3
+// | |-- f3u4, http://www.f3u4.com/
+// | |-- f3u2, http://www.f3u2.com/
+// | |-- f3u3, http://www.f3u3.com/
+// | +-- f3u1, http://www.f3u1.com/
+// |-- u4, http://www.u4.com/
+// |-- u3, http://www.u3.com/
+// --- f4
+// | |-- f4u1, http://www.f4u1.com/
+// | |-- f4u2, http://www.f4u2.com/
+// | |-- f4u3, http://www.f4u3.com/
+// | +-- f4u4, http://www.f4u4.com/
+// |-- dup
+// | +-- dupu1, http://www.dupu1.com/
+// +-- dup
+// +-- dupu2, http://www.dupu1.com/
+//
+static TestData kBookmarkBarChildren[] = {
+ { L"u2", L"http://www.u2.com/" },
+ { L"f1", NULL },
+ { L"u1", L"http://www.u1.com/" },
+ { L"f2", NULL },
+};
+static TestData kF1Children[] = {
+ { L"f1u4", L"http://www.f1u4.com/" },
+ { L"f1u2", L"http://www.f1u2.com/" },
+ { L"f1u3", L"http://www.f1u3.com/" },
+ { L"f1u1", L"http://www.f1u1.com/" },
+};
+static TestData kF2Children[] = {
+ { L"f2u2", L"http://www.f2u2.com/" },
+ { L"f2u4", L"http://www.f2u4.com/" },
+ { L"f2u3", L"http://www.f2u3.com/" },
+ { L"f2u1", L"http://www.f2u1.com/" },
+};
+
+static TestData kOtherBookmarksChildren[] = {
+ { L"f3", NULL },
+ { L"u4", L"http://www.u4.com/" },
+ { L"u3", L"http://www.u3.com/" },
+ { L"f4", NULL },
+ { L"dup", NULL },
+ { L"dup", NULL },
+};
+static TestData kF3Children[] = {
+ { L"f3u4", L"http://www.f3u4.com/" },
+ { L"f3u2", L"http://www.f3u2.com/" },
+ { L"f3u3", L"http://www.f3u3.com/" },
+ { L"f3u1", L"http://www.f3u1.com/" },
+};
+static TestData kF4Children[] = {
+ { L"f4u1", L"http://www.f4u1.com/" },
+ { L"f4u2", L"http://www.f4u2.com/" },
+ { L"f4u3", L"http://www.f4u3.com/" },
+ { L"f4u4", L"http://www.f4u4.com/" },
+};
+static TestData kDup1Children[] = {
+ { L"dupu1", L"http://www.dupu1.com/" },
+};
+static TestData kDup2Children[] = {
+ { L"dupu2", L"http://www.dupu2.com/" },
+};
+
+} // anonymous namespace.
+
+void ProfileSyncServiceTestWithData::PopulateFromTestData(
+ const BookmarkNode* node, const TestData* data, int size) {
+ DCHECK(node);
+ DCHECK(data);
+ DCHECK(node->is_folder());
+ for (int i = 0; i < size; ++i) {
+ const TestData& item = data[i];
+ if (item.url) {
+ model_->AddURL(node, i, item.title, GURL(item.url));
+ } else {
+ model_->AddGroup(node, i, item.title);
+ }
+ }
+}
+
+void ProfileSyncServiceTestWithData::CompareWithTestData(
+ const BookmarkNode* node, const TestData* data, int size) {
+ DCHECK(node);
+ DCHECK(data);
+ DCHECK(node->is_folder());
+ for (int i = 0; i < size; ++i) {
+ const BookmarkNode* child_node = node->GetChild(i);
+ const TestData& item = data[i];
+ EXPECT_TRUE(child_node->GetTitle() == item.title);
+ if (item.url) {
+ EXPECT_FALSE(child_node->is_folder());
+ EXPECT_TRUE(child_node->is_url());
+ EXPECT_TRUE(child_node->GetURL() == GURL(item.url));
+ } else {
+ EXPECT_TRUE(child_node->is_folder());
+ EXPECT_FALSE(child_node->is_url());
+ }
+ }
+}
+
+// TODO(munjal): We should implement some way of generating random data and can
+// use the same seed to generate the same sequence.
+void ProfileSyncServiceTestWithData::WriteTestDataToBookmarkModel() {
+ const BookmarkNode* bookmarks_bar_node = model_->GetBookmarkBarNode();
+ PopulateFromTestData(bookmarks_bar_node,
+ kBookmarkBarChildren,
+ arraysize(kBookmarkBarChildren));
+
+ ASSERT_GE(bookmarks_bar_node->GetChildCount(), 4);
+ const BookmarkNode* f1_node = bookmarks_bar_node->GetChild(1);
+ PopulateFromTestData(f1_node, kF1Children, arraysize(kF1Children));
+ const BookmarkNode* f2_node = bookmarks_bar_node->GetChild(3);
+ PopulateFromTestData(f2_node, kF2Children, arraysize(kF2Children));
+
+ const BookmarkNode* other_bookmarks_node = model_->other_node();
+ PopulateFromTestData(other_bookmarks_node,
+ kOtherBookmarksChildren,
+ arraysize(kOtherBookmarksChildren));
+
+ ASSERT_GE(other_bookmarks_node->GetChildCount(), 6);
+ const BookmarkNode* f3_node = other_bookmarks_node->GetChild(0);
+ PopulateFromTestData(f3_node, kF3Children, arraysize(kF3Children));
+ const BookmarkNode* f4_node = other_bookmarks_node->GetChild(3);
+ PopulateFromTestData(f4_node, kF4Children, arraysize(kF4Children));
+ const BookmarkNode* dup_node = other_bookmarks_node->GetChild(4);
+ PopulateFromTestData(dup_node, kDup1Children, arraysize(kDup1Children));
+ dup_node = other_bookmarks_node->GetChild(5);
+ PopulateFromTestData(dup_node, kDup2Children, arraysize(kDup2Children));
+
+ ExpectBookmarkModelMatchesTestData();
+}
+
+void ProfileSyncServiceTestWithData::ExpectBookmarkModelMatchesTestData() {
+ const BookmarkNode* bookmark_bar_node = model_->GetBookmarkBarNode();
+ CompareWithTestData(bookmark_bar_node,
+ kBookmarkBarChildren,
+ arraysize(kBookmarkBarChildren));
+
+ ASSERT_GE(bookmark_bar_node->GetChildCount(), 4);
+ const BookmarkNode* f1_node = bookmark_bar_node->GetChild(1);
+ CompareWithTestData(f1_node, kF1Children, arraysize(kF1Children));
+ const BookmarkNode* f2_node = bookmark_bar_node->GetChild(3);
+ CompareWithTestData(f2_node, kF2Children, arraysize(kF2Children));
+
+ const BookmarkNode* other_bookmarks_node = model_->other_node();
+ CompareWithTestData(other_bookmarks_node,
+ kOtherBookmarksChildren,
+ arraysize(kOtherBookmarksChildren));
+
+ ASSERT_GE(other_bookmarks_node->GetChildCount(), 6);
+ const BookmarkNode* f3_node = other_bookmarks_node->GetChild(0);
+ CompareWithTestData(f3_node, kF3Children, arraysize(kF3Children));
+ const BookmarkNode* f4_node = other_bookmarks_node->GetChild(3);
+ CompareWithTestData(f4_node, kF4Children, arraysize(kF4Children));
+ const BookmarkNode* dup_node = other_bookmarks_node->GetChild(4);
+ CompareWithTestData(dup_node, kDup1Children, arraysize(kDup1Children));
+ dup_node = other_bookmarks_node->GetChild(5);
+ CompareWithTestData(dup_node, kDup2Children, arraysize(kDup2Children));
+}
+
+// Tests persistence of the profile sync service by destroying the
+// profile sync service and then reloading it from disk.
+TEST_F(ProfileSyncServiceTestWithData, Persistence) {
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, SAVE_TO_STORAGE);
+ StartSyncService();
+
+ WriteTestDataToBookmarkModel();
+
+ ExpectModelMatch();
+
+ // Force both models to discard their data and reload from disk. This
+ // simulates what would happen if the browser were to shutdown normally,
+ // and then relaunch.
+ StopSyncService(SAVE_TO_STORAGE);
+ LoadBookmarkModel(LOAD_FROM_STORAGE, SAVE_TO_STORAGE);
+ StartSyncService();
+
+ ExpectBookmarkModelMatchesTestData();
+
+ // With the BookmarkModel contents verified, ExpectModelMatch will
+ // verify the contents of the sync model.
+ ExpectModelMatch();
+}
+
+// Tests the merge case when the BookmarkModel is non-empty but the
+// sync model is empty. This corresponds to uploading browser
+// bookmarks to an initially empty, new account.
+TEST_F(ProfileSyncServiceTestWithData, MergeWithEmptySyncModel) {
+ // Don't start the sync service until we've populated the bookmark model.
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, SAVE_TO_STORAGE);
+
+ WriteTestDataToBookmarkModel();
+
+ // Restart the profile sync service. This should trigger a merge step
+ // during initialization -- we expect the browser bookmarks to be written
+ // to the sync service during this call.
+ StartSyncService();
+
+ // Verify that the bookmark model hasn't changed, and that the sync model
+ // matches it exactly.
+ ExpectBookmarkModelMatchesTestData();
+ ExpectModelMatch();
+}
+
+// Tests the merge case when the BookmarkModel is empty but the sync model is
+// non-empty. This corresponds (somewhat) to a clean install of the browser,
+// with no bookmarks, connecting to a sync account that has some bookmarks.
+TEST_F(ProfileSyncServiceTestWithData, MergeWithEmptyBookmarkModel) {
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, DONT_SAVE_TO_STORAGE);
+ StartSyncService();
+
+ WriteTestDataToBookmarkModel();
+
+ ExpectModelMatch();
+
+ // Force the sync service to shut down and write itself to disk.
+ StopSyncService(SAVE_TO_STORAGE);
+
+ // Blow away the bookmark model -- it should be empty afterwards.
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, DONT_SAVE_TO_STORAGE);
+ EXPECT_EQ(model_->GetBookmarkBarNode()->GetChildCount(), 0);
+ EXPECT_EQ(model_->other_node()->GetChildCount(), 0);
+
+ // Now restart the sync service. Starting it should populate the bookmark
+ // model -- test for consistency.
+ StartSyncService();
+ ExpectBookmarkModelMatchesTestData();
+ ExpectModelMatch();
+}
+
+// Tests the merge cases when both the models are expected to be identical
+// after the merge.
+TEST_F(ProfileSyncServiceTestWithData, MergeExpectedIdenticalModels) {
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, SAVE_TO_STORAGE);
+ StartSyncService();
+ WriteTestDataToBookmarkModel();
+ ExpectModelMatch();
+ StopSyncService(SAVE_TO_STORAGE);
+
+ // At this point both the bookmark model and the server should have the
+ // exact same data and it should match the test data.
+ LoadBookmarkModel(LOAD_FROM_STORAGE, DONT_SAVE_TO_STORAGE);
+ StartSyncService();
+ ExpectBookmarkModelMatchesTestData();
+ ExpectModelMatch();
+ StopSyncService(SAVE_TO_STORAGE);
+
+ // Now reorder some bookmarks in the bookmark model and then merge. Make
+ // sure we get the order of the server after merge.
+ LoadBookmarkModel(LOAD_FROM_STORAGE, DONT_SAVE_TO_STORAGE);
+ ExpectBookmarkModelMatchesTestData();
+ const BookmarkNode* bookmark_bar = model_->GetBookmarkBarNode();
+ ASSERT_TRUE(bookmark_bar);
+ ASSERT_GT(bookmark_bar->GetChildCount(), 1);
+ model_->Move(bookmark_bar->GetChild(0), bookmark_bar, 1);
+ StartSyncService();
+ ExpectModelMatch();
+ ExpectBookmarkModelMatchesTestData();
+}
+
+// Tests the merge cases when both the models are expected to be identical
+// after the merge.
+TEST_F(ProfileSyncServiceTestWithData, MergeModelsWithSomeExtras) {
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, DONT_SAVE_TO_STORAGE);
+ WriteTestDataToBookmarkModel();
+ ExpectBookmarkModelMatchesTestData();
+
+ // Remove some nodes and reorder some nodes.
+ const BookmarkNode* bookmark_bar_node = model_->GetBookmarkBarNode();
+ int remove_index = 2;
+ ASSERT_GT(bookmark_bar_node->GetChildCount(), remove_index);
+ const BookmarkNode* child_node = bookmark_bar_node->GetChild(remove_index);
+ ASSERT_TRUE(child_node);
+ ASSERT_TRUE(child_node->is_url());
+ model_->Remove(bookmark_bar_node, remove_index);
+ ASSERT_GT(bookmark_bar_node->GetChildCount(), remove_index);
+ child_node = bookmark_bar_node->GetChild(remove_index);
+ ASSERT_TRUE(child_node);
+ ASSERT_TRUE(child_node->is_folder());
+ model_->Remove(bookmark_bar_node, remove_index);
+
+ const BookmarkNode* other_node = model_->other_node();
+ ASSERT_GE(other_node->GetChildCount(), 1);
+ const BookmarkNode* f3_node = other_node->GetChild(0);
+ ASSERT_TRUE(f3_node);
+ ASSERT_TRUE(f3_node->is_folder());
+ remove_index = 2;
+ ASSERT_GT(f3_node->GetChildCount(), remove_index);
+ model_->Remove(f3_node, remove_index);
+ ASSERT_GT(f3_node->GetChildCount(), remove_index);
+ model_->Remove(f3_node, remove_index);
+
+ StartSyncService();
+ ExpectModelMatch();
+ StopSyncService(SAVE_TO_STORAGE);
+
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, DONT_SAVE_TO_STORAGE);
+ WriteTestDataToBookmarkModel();
+ ExpectBookmarkModelMatchesTestData();
+
+ // Remove some nodes and reorder some nodes.
+ bookmark_bar_node = model_->GetBookmarkBarNode();
+ remove_index = 0;
+ ASSERT_GT(bookmark_bar_node->GetChildCount(), remove_index);
+ child_node = bookmark_bar_node->GetChild(remove_index);
+ ASSERT_TRUE(child_node);
+ ASSERT_TRUE(child_node->is_url());
+ model_->Remove(bookmark_bar_node, remove_index);
+ ASSERT_GT(bookmark_bar_node->GetChildCount(), remove_index);
+ child_node = bookmark_bar_node->GetChild(remove_index);
+ ASSERT_TRUE(child_node);
+ ASSERT_TRUE(child_node->is_folder());
+ model_->Remove(bookmark_bar_node, remove_index);
+
+ ASSERT_GE(bookmark_bar_node->GetChildCount(), 2);
+ model_->Move(bookmark_bar_node->GetChild(0), bookmark_bar_node, 1);
+
+ other_node = model_->other_node();
+ ASSERT_GE(other_node->GetChildCount(), 1);
+ f3_node = other_node->GetChild(0);
+ ASSERT_TRUE(f3_node);
+ ASSERT_TRUE(f3_node->is_folder());
+ remove_index = 0;
+ ASSERT_GT(f3_node->GetChildCount(), remove_index);
+ model_->Remove(f3_node, remove_index);
+ ASSERT_GT(f3_node->GetChildCount(), remove_index);
+ model_->Remove(f3_node, remove_index);
+
+ ASSERT_GE(other_node->GetChildCount(), 4);
+ model_->Move(other_node->GetChild(0), other_node, 1);
+ model_->Move(other_node->GetChild(2), other_node, 3);
+
+ StartSyncService();
+ ExpectModelMatch();
+
+ // After the merge, the model should match the test data.
+ ExpectBookmarkModelMatchesTestData();
+}
+
+// Tests that when persisted model assocations are used, things work fine.
+TEST_F(ProfileSyncServiceTestWithData, ModelAssociationPersistence) {
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, DONT_SAVE_TO_STORAGE);
+ WriteTestDataToBookmarkModel();
+ StartSyncService();
+ ExpectModelMatch();
+ // Force the sync service to shut down and write itself to disk.
+ StopSyncService(SAVE_TO_STORAGE);
+ // Now restart the sync service. This time it should use the persistent
+ // assocations.
+ StartSyncService();
+ ExpectModelMatch();
+}
+
+TEST_F(ProfileSyncServiceTestWithData, SortChildren) {
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, DONT_SAVE_TO_STORAGE);
+ StartSyncService();
+
+ // Write test data to bookmark model and verify that the models match.
+ WriteTestDataToBookmarkModel();
+ const BookmarkNode* folder_added = model_->other_node()->GetChild(0);
+ ASSERT_TRUE(folder_added);
+ ASSERT_TRUE(folder_added->is_folder());
+
+ ExpectModelMatch();
+
+ // Sort the other-bookmarks children and expect that hte models match.
+ model_->SortChildren(folder_added);
+ ExpectModelMatch();
+}
+
+// See what happens if we enable sync but then delete the "Sync Data"
+// folder.
+TEST_F(ProfileSyncServiceTestWithData, RecoverAfterDeletingSyncDataDirectory) {
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, SAVE_TO_STORAGE);
+ StartSyncService();
+
+ WriteTestDataToBookmarkModel();
+
+ // While the service is running.
+ FilePath sync_data_directory = backend()->sync_data_folder_path();
+
+ // Simulate a normal shutdown for the sync service (don't disable it for
+ // the user, which would reset the preferences and delete the sync data
+ // directory).
+ StopSyncService(SAVE_TO_STORAGE);
+
+ // Now pretend that the user has deleted this directory from the disk.
+ file_util::Delete(sync_data_directory, true);
+
+ // Restart the sync service.
+ StartSyncService();
+
+ // Make sure we're back in sync. In real life, the user would need
+ // to reauthenticate before this happens, but in the test, authentication
+ // is sidestepped.
+ ExpectBookmarkModelMatchesTestData();
+ ExpectModelMatch();
+}
+
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/sync/resources/about_sync.html b/chrome/browser/sync/resources/about_sync.html
new file mode 100644
index 0000000..0fbd6df
--- /dev/null
+++ b/chrome/browser/sync/resources/about_sync.html
@@ -0,0 +1,165 @@
+<html>
+</html>
+<html id="t">
+<head>
+<title>About Sync</title>
+
+<style type="text/css">
+body {
+ font-size: 84%;
+ font-family: Arial, Helvetica, sans-serif;
+ padding: 0.75em;
+ margin: 0;
+ min-width: 45em;
+}
+
+h1 {
+ font-size: 110%;
+ font-weight: bold;
+ color: #4a8ee6;
+ letter-spacing: -1px;
+ padding: 0;
+ margin: 0;
+}
+h2 {
+ font-size: 110%;
+ letter-spacing: -1px;
+ font-weight: normal;
+ color: #4a8ee6;
+ padding: 0;
+ margin: 0;
+ padding: 0.5em 1em;
+ color: #3a75bd;
+ margin-left: -38px;
+ padding-left: 38px;
+
+ border-top: 1px solid #3a75bd;
+ padding-top: 0.5em;
+
+}
+h2:first-child {
+ border-top: 0;
+ padding-top: 0;
+}
+
+div#header {
+ padding: 0.75em 1em;
+ padding-top: 0.6em;
+ padding-left: 0;
+ margin-bottom: 0.75em;
+ position: relative;
+ overflow: hidden;
+ background: #5296de;
+ -webkit-background-size: 100%;
+ border: 1px solid #3a75bd;
+ -webkit-border-radius: 6px;
+ color: white;
+ text-shadow: 0 0 2px black;
+}
+div#header h1 {
+ padding-left: 37px;
+ margin: 0;
+ display: inline;
+ background: url('gear.png') 12px 60% no-repeat;
+ color: white;
+}
+div#header p {
+ font-size: 84%;
+ font-style: italic;
+ padding: 0;
+ margin: 0;
+ color: white;
+ padding-left: 0.4em;
+ display: inline;
+}
+
+table.list {
+ line-height: 200%;
+ border-collapse: collapse;
+ font-size: 84%;
+ table-layout: fixed;
+}
+table.list:not([class*='filtered']) tr:nth-child(odd) td {
+ background: #eff3ff;
+}
+
+table.list td {
+ padding: 0 0.5em;
+ vertical-align: top;
+ line-height: 1.4em;
+ padding-top: 0.35em;
+}
+table.list tr td:nth-last-child(1),
+table.list tr th:nth-last-child(1) {
+ padding-right: 1em;
+}
+table.list:not([class*='filtered']) .tab .name {
+ padding-left: 1.5em;
+}
+
+table.list .name {
+}
+
+table.list .name div {
+ height: 1.6em;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+table.list .number {
+ width: 7em;
+ text-align: right;
+}
+
+table.list#details tr:not([class*='firstRow']) > *:nth-child(1),
+table.list#details tr:not([class*='firstRow']) > *:nth-child(4),
+table.list#details tr.firstRow th:nth-child(1),
+table.list#details tr.firstRow th:nth-child(2) {
+ border-right: 1px solid #b5c6de;
+}
+table.list#details .name {
+ padding-left: 25px;
+ background-position: 5px center;
+ background-repeat: no-repeat;
+}
+</style>
+</head>
+<body>
+ <div id='header'>
+ <h1>About Sync</h1>
+ <p> Sync engine diagnostic data</p>
+ </div>
+ <div id='content'>
+ <h2> Summary </h2>
+ <strong jscontent="summary"></strong>
+ <br /><br /><br />
+ <h2> Details </h2>
+ <table class='list' id='details'>
+ <tr>
+ <td class='name'> Authenticated </td>
+ <td class='number'>
+ <div jscontent="authenticated"> </div>
+ <div jsdisplay="!authenticated"
+ style="color:red"
+ jscontent="auth_problem"></div>
+ </td>
+ </tr>
+ </tr>
+ <tr>
+ <td class='name'>Last Synced</td>
+ <td class='number' jscontent="time_since_sync"> </td>
+ </tr>
+ <tr jsselect="details">
+ <td class='name'>
+ <div jscontent="stat_name"></div>
+ </td>
+ <td class='number'>
+ <div jscontent="stat_value"></div>
+ </td>
+ </tr>
+ </table>
+ </div>
+</body>
+</html>
+
diff --git a/chrome/browser/sync/resources/gaia_login.html b/chrome/browser/sync/resources/gaia_login.html
new file mode 100644
index 0000000..17cc102
--- /dev/null
+++ b/chrome/browser/sync/resources/gaia_login.html
@@ -0,0 +1,331 @@
+<html>
+<style type="text/css"><!--
+ body,td,div,p,a,font,span {font-family: arial,sans-serif;}
+ body { bgcolor:"#ffffff" }
+ A:link {color:#0000cc; }
+ A:visited { color:#551a8b; }
+ A:active { color:#ff0000; }
+ .form-noindent {background-color: #ffffff; border: #C3D9FF 1px solid}
+--></style>
+ <head>
+ <style type="text/css"><!--
+ .body { margin-left: 3em;
+ margin-right: 5em;
+ font-family: arial,sans-serif; }
+ div.errorbox-good {}
+ div.errorbox-bad {}
+ div.errormsg { color: red; font-size: smaller;
+ font-family: arial,sans-serif;}
+ font.errormsg { color: red; font-size: smaller;
+ font-family: arial,sans-serif;}
+ hr {
+ border: 0;
+ background-color:#DDDDDD;
+ height: 1px;
+ width: 100%;
+ text-align: left;
+ margin: 5px;
+ }
+ --></style>
+ </head>
+ <body dir="ltr" bgcolor="#ffffff" vlink="#666666"
+ style="margin-bottom: 0" onload="initForm();">
+ <table width="100%" align="center" cellpadding="1" cellspacing="1">
+ <tr>
+ <td valign="top"> <!-- LOGIN BOX -->
+ <script>
+ function gaia_setFocus() {
+ var f = null;
+ if (document.getElementById) {
+ f = document.getElementById("gaia_loginform");
+ } else if (window.gaia_loginform) {
+ f = window.gaia_loginform;
+ }
+ if (f) {
+ if (f.Email && (f.Email.value == null || f.Email.value == "")) {
+ f.Email.focus();
+ } else if (f.Passwd) {
+ f.Passwd.focus();
+ }
+ }
+ }
+
+ function advanceThrobber() {
+ var throbber = document.getElementById('throb');
+ throbber.style.backgroundPositionX =
+ ((parseInt(throbber.style.backgroundPositionX) - 16) % 576) + 'px';
+ }
+
+ function showGaiaLogin(args) {
+ var throbber = document.getElementById('throbber_container');
+ throbber.style.display = "none";
+ var f = document.getElementById("gaia_loginform");
+ if (f) {
+ f.Email.value = args.user;
+ }
+ resetErrorVisibility();
+ var t = document.getElementById("errormsg_1_Password");
+ if (t) {
+ t.innerHTML = "Username and password do not match. [<a href=\"http://www.google.com/support/accounts/bin/answer.py?ctx=ch&answer=27444\">?</a>]";
+ }
+ if (1 == args.error) {
+ setElementDisplay("errormsg_1_Password", 'table-row');
+ setBlurbError();
+ }
+ if (3 == args.error) {
+ setElementDisplay("errormsg_0_Connection", 'table-row');
+ setBlurbError();
+ }
+ document.getElementById("signIn").disabled = false;
+ gaia_setFocus();
+ }
+
+ function CloseDialog() {
+ chrome.send("DialogClose", [""]);
+ }
+
+ function showGaiaSuccessAndClose() {
+ document.getElementById("signIn").value = "Success!";
+ setTimeout(CloseDialog, 1600);
+ }
+
+ function showGaiaSuccessAndSettingUp() {
+ document.getElementById("signIn").value = "Setting up...";
+ }
+
+ function initForm() {
+ setInterval(advanceThrobber, 30);
+ var args = JSON.parse(chrome.dialogArguments);
+ showGaiaLogin(args);
+ }
+
+ function sendCredentialsAndClose() {
+ if (!setErrorVisibility())
+ return false;
+
+ var throbber = document.getElementById('throbber_container');
+ throbber.style.display = "inline";
+ var f = document.getElementById("gaia_loginform");
+ var result = JSON.stringify({"user" : f.Email.value,
+ "pass" : f.Passwd.value});
+ document.getElementById("signIn").disabled = true;
+ chrome.send("SubmitAuth", [result]);
+ }
+
+ function setElementDisplay(id, display) {
+ var d = document.getElementById(id);
+ if (d)
+ d.style.display = display;
+ }
+
+ function setBlurbError() {
+ var blurb = document.getElementById("top_blurb");
+ blurb.innerHTML =
+ '<font size="-1">Setting up Bookmarks Sync<br/><br/><b>Error signing in.</b></font>';
+ }
+
+ function resetErrorVisibility() {
+ setElementDisplay("errormsg_0_Email", 'none');
+ setElementDisplay("errormsg_0_Password", 'none');
+ setElementDisplay("errormsg_1_Password", 'none');
+ setElementDisplay("errormsg_0_Connection", 'none');
+ }
+
+ function setErrorVisibility() {
+ resetErrorVisibility();
+ var f = document.getElementById("gaia_loginform");
+ if (null == f.Email.value || "" == f.Email.value) {
+ setElementDisplay("errormsg_0_Email", 'table-row');
+ setBlurbError();
+ return false;
+ }
+ if (null == f.Passwd.value || "" == f.Passwd.value) {
+ setElementDisplay("errormsg_0_Password", 'table-row');
+ setBlurbError();
+ return false;
+ }
+ return true;
+ }
+ </script>
+<style type="text/css"><!--
+ div.errormsg { color: red; font-size: smaller; font-family:arial,sans-serif; }
+ font.errormsg { color: red; font-size: smaller; font-family:arial,sans-serif;}
+--></style>
+<style type="text/css"><!--
+.gaia.le.lbl { font-family: Arial, Helvetica, sans-serif; font-size: smaller; }
+.gaia.le.fpwd { font-family: Arial, Helvetica, sans-serif; font-size: 70%; }
+.gaia.le.chusr { font-family: Arial, Helvetica, sans-serif; font-size: 70%; }
+.gaia.le.val { font-family: Arial, Helvetica, sans-serif; font-size: smaller; }
+.gaia.le.button { font-family: Arial, Helvetica, sans-serif; font-size: smaller; }
+.gaia.le.rem { font-family: Arial, Helvetica, sans-serif; font-size: smaller; }
+.gaia.captchahtml.desc { font-family: arial, sans-serif; font-size: smaller; }
+.gaia.captchahtml.cmt { font-family: arial, sans-serif; font-size: smaller;
+ font-style: italic; }
+--></style>
+<p id="top_blurb"> <font size="-1">
+Google Chrome can sync your bookmarks with your Google account &ndash;
+bookmarks you create on this computer will be made instantly visible on all the
+computers synced to the same account.</font></p>
+<form id="gaia_loginform" onsubmit="sendCredentialsAndClose(); return false;">
+<div id="gaia_loginbox">
+<table class="form-noindent" cellspacing="3" cellpadding="5" width="100%"
+ border="0">
+ <tr>
+ <td valign="top" style="text-align:center" nowrap="nowrap"
+ bgcolor="#e8eefa">
+ <div class="loginBox">
+ <table id="gaia_table" align="center" border="0" cellpadding="1"
+ cellspacing="0">
+ <tr>
+ <td colspan="2" align="center">
+ <font size="-1"> Sign in with your </font>
+ <table>
+ <tr>
+ <td valign="top">
+ <img src="google_transparent.png" alt="Google">
+ </img>
+ </td>
+ <td valign="middle">
+ <font size="+0"><b>Account</b> </font>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <script type="text/javascript"><!--
+ function onPreCreateAccount() {
+ return true;
+ }
+ function onPreLogin() {
+ if (window["onlogin"] != null) {
+ return onlogin();
+ } else {
+ return true;
+ }
+ }
+ --></script>
+ <tr>
+ <td colspan="2" align="center"> </td>
+ </tr>
+ <tr>
+ <td nowrap="nowrap">
+ <div align="right">
+ <span class="gaia le lbl">
+ Email:
+ </span>
+ </div>
+ </td>
+ <td>
+ <input type="text" name="Email" id="Email" size="18"
+ value="" class='gaia le val' />
+ </td>
+ </tr>
+ <tr>
+ <td></td>
+ <td align="left">
+ <div class="errormsg" id="errormsg_0_Email">
+ Required field cannot be left blank
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td></td>
+ <td align="left"> </td>
+ </tr>
+ <tr>
+ <td align="right">
+ <span class="gaia le lbl">
+ Password:
+ </span>
+ </td>
+ <td>
+ <input type="password" name="Passwd" id="Passwd" size="18"
+ class="gaia le val"/>
+ </td>
+ </tr>
+ <tr>
+ <td></td>
+ <td align="left">
+ <div class="errormsg" id="errormsg_0_Password">
+ Required field cannot be left blank
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ </td>
+ <td align="left">
+ <div class="errormsg" id="errormsg_1_Password">
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ </td>
+ <td align="left">
+ <div class="errormsg" id="errormsg_0_Connection">
+ Could not connect to the server
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ </td>
+ <td align="left">
+ </td>
+ </tr>
+ <tr>
+ <td>
+ </td>
+ <td align="left">
+ <table>
+ <tr>
+ <td>
+ <div id="throbber_container" style="display:none">
+ <div id="throb" style="background-image:url(throbber.png);
+ width:16px; height:16px; background-position:0px;">
+ </div>
+ </div>
+ </td>
+ <td>
+ <input id="signIn" type="button" class="gaia le button"
+ name="signIn" value="Sign in"
+ onclick="sendCredentialsAndClose();" />
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr id="ga-fprow">
+ <td colspan="2" height="16.0" class="gaia le fpwd"
+ align="center" valign="bottom">
+ <a href="http://www.google.com/support/accounts/bin/answer.py?answer=48598&hl=en&fpUrl=https%3A%2F%2Fwww.google.com%2Faccounts%2FForgotPasswd%3FfpOnly%3D1%26service%3Dchromiumsync"
+ target=_blank>
+ I cannot access my account
+ </a>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2" height="16.0" class="gaia le fpwd"
+ align="center" valign="bottom">
+ <a href="javascript:var popup=window.open('https%3A%5Cx2F%5Cx2Fwww.google.com%5Cx2Faccounts%5Cx2FNewAccount%3Fservice%3Dchromiumsync', 'NewAccount', 'height=870,width=870,resizable=yes,scrollbars=yes');">
+ Create a Google account
+ </a>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </td>
+ </tr>
+</table>
+</div>
+</form>
+</td>
+</tr>
+ </table>
+ <div align="right">
+ <input type="button" name="cancel" value="Cancel" onclick="CloseDialog();"/>
+ </div>
+ </table>
+</body>
+</html>
diff --git a/chrome/browser/sync/resources/merge_and_sync.html b/chrome/browser/sync/resources/merge_and_sync.html
new file mode 100644
index 0000000..58a769b
--- /dev/null
+++ b/chrome/browser/sync/resources/merge_and_sync.html
@@ -0,0 +1,66 @@
+<HTML>
+<HEAD>
+<TITLE></TITLE>
+<style type="text/css">
+ body,td,div,p,a,font,span {font-family: arial,sans-serif;}
+ body { bgcolor:"#ffffff" }
+.gaia.le.button { font-family: Arial, Helvetica, sans-serif; font-size: smaller; }
+</style>
+<script>
+ function advanceThrobber() {
+ var throbber = document.getElementById('throb');
+ throbber.style.backgroundPositionX =
+ ((parseInt(throbber.style.backgroundPositionX) - 16) % 576) + 'px';
+ }
+
+ function acceptMergeAndSync() {
+ var throbber = document.getElementById('throbber_container');
+ throbber.style.display = "inline";
+ document.getElementById("acceptMerge").disabled = true;
+ chrome.send("SubmitMergeAndSync", [""]);
+ }
+
+ function Close() {
+ chrome.send("DialogClose", [""]);
+ }
+
+ function showMergeAndSyncDone() {
+ var throbber = document.getElementById('throbber_container');
+ throbber.style.display = "none";
+ document.getElementById("header").innerHTML =
+ "<font size='-1'><b>All done!</b></font>";
+ document.getElementById("close").value = "Close";
+ setTimeout(Close, 1600);
+ }
+</script>
+</HEAD>
+<BODY onload="setInterval(advanceThrobber, 30);">
+<p id="header"><font size="-1"><b>Your bookmarks will be merged.</b></font></p><br />
+<img src="merge_and_sync.png" alt="Merge and sync" />
+<br />
+<p><font size="-1">
+Your existing online bookmarks will be merged with the
+bookmarks on this machine. You can use the Bookmark Manager to organize
+your bookmarks after the merge.</font></p>
+<br />
+<table align="right">
+ <tr>
+ <td>
+ <div id="throbber_container" style="display:none">
+ <div id="throb" style="background-image:url(throbber.png);
+ width:16px; height:16px; background-position:0px;">
+ </div>
+ </div>
+ </td>
+ <td>
+ <input id="acceptMerge" type="button" class="gaia le button" name="accept"
+ value="Merge and sync"
+ onclick="acceptMergeAndSync();" />
+ </td>
+ <td>
+ <input id="close" type="button" value="Abort" onclick="Close();"/>
+ </td>
+ </tr>
+</table>
+</BODY>
+</HTML>
diff --git a/chrome/browser/sync/resources/new_tab_personalization.html b/chrome/browser/sync/resources/new_tab_personalization.html
new file mode 100644
index 0000000..73cf346
--- /dev/null
+++ b/chrome/browser/sync/resources/new_tab_personalization.html
@@ -0,0 +1,117 @@
+<html>
+<head>
+<style type="text/css">
+body {
+ font-family:arial;
+ background-color:white;
+ font-size:80%;
+ margin:0px;
+}
+.section-title {
+ color:#000;
+ line-height:19pt;
+ font-size:95%;
+ font-weight:bold;
+ margin-bottom:4px;
+ margin-left: 0px;
+}
+a {
+ color:#0000cc;
+ white-space: nowrap;
+}
+.sidebar {
+ width: 207px;
+ padding:3px 10px 3px 9px;
+ -webkit-border-radius:5px 5px;
+ margin-bottom:10px;
+}
+</style>
+<script>
+function resizeFrame(newsize) {
+ chrome.send("ResizeP13N", [newsize.toString()]);
+}
+</script>
+</head>
+<body>
+<div id="sync" class="sidebar">
+ <table id="titletable" width="200" cellpadding="0" cellspacing="0"
+ style="display:none">
+ <tr>
+ <td id="messagetitle" align="left" class="section-title">
+ </td>
+ <td align="right">
+ <a href="#" onclick="resizeFrame(0);">
+ <img id="greenclose" src="close.png"/>
+ </a>
+ </td>
+ </tr>
+ </table>
+ <div id="syncContainer"></div>
+</div>
+<script>
+/* Return a DOM element with tag name |elem| and attributes |attrs|. */
+function DOM(elem, attrs) {
+ var elem = document.createElement(elem);
+ for (var attr in attrs) {
+ elem[attr] = attrs[attr];
+ }
+ return elem;
+}
+
+function renderSyncMessage(message) {
+ var section = document.getElementById('sync');
+ var container = document.getElementById('syncContainer');
+ var title = document.getElementById('messagetitle');
+ var titletable = document.getElementById('titletable');
+ container.innerHTML = '';
+ title.innerHTML = '';
+ titletable.style.display = "none";
+ section.style.display = "block";
+
+ /* Set the sync section background color. */
+ if (message.msgtype == "error") {
+ section.style.backgroundColor = "#f8d1ca";
+ } else if (message.msgtype == "presynced") {
+ section.style.backgroundColor = "#e0f8ca";
+ } else {
+ section.style.backgroundColor = "#e1ecfe";
+ }
+
+ if (message.msgtype != "synced") {
+ /* Any message except the status normal / synced to
+ message requires extra markup for a title, close button,
+ and links. */
+ var titletxt = document.createTextNode(message.title);
+ title.appendChild(titletxt);
+ titletable.style.display = "block";
+ }
+
+ /* The main message of the sync section. */
+ var txt = DOM('p');
+ txt.style.margin = 0;
+ txt.appendChild(document.createTextNode(message.msg));
+ container.appendChild(txt);
+
+ /* If we should show a link, create the href. */
+ if (message.linktext) {
+ var link = DOM('a', { href:"#", title: message.linktext});
+ link.onclick = function(tt) {
+ return function() {
+ chrome.send("SyncLinkClicked", [tt]);
+ return false;
+ }
+ } (message.title);
+
+ /* Tie it together. */
+ link.appendChild(document.createTextNode(message.linktext));
+ container.appendChild(link);
+ }
+
+ /* Tell our container to resize to fit us appropriately. */
+ resizeFrame(document.body.scrollHeight);
+}
+
+chrome.send("GetSyncMessage");
+</script>
+</body>
+</html> \ No newline at end of file
diff --git a/chrome/browser/sync/resources/setup_flow.html b/chrome/browser/sync/resources/setup_flow.html
new file mode 100644
index 0000000..82db8a9
--- /dev/null
+++ b/chrome/browser/sync/resources/setup_flow.html
@@ -0,0 +1,19 @@
+<HTML id='t'>
+<style type="text/css">
+</style>
+<HEAD>
+<TITLE></TITLE>
+<script>
+ function showMergeAndSync() {
+ document.getElementById("login").style.display = "none";
+ document.getElementById("merge").style.display = "block";
+ }
+</script>
+</HEAD>
+<BODY style="margin:0; border:0;">
+ <iframe id="login" frameborder="0" width="100%" scrolling="no" height="100%"
+ src="cloudy://resources/gaialogin"></iframe>
+ <iframe id="merge" frameborder="0" width="100%" scrolling="no" height="100%"
+ src="cloudy://resources/mergeandsync" style="display:none"></iframe>
+</BODY>
+</HTML>
diff --git a/chrome/browser/sync/sync_status_ui_helper.cc b/chrome/browser/sync/sync_status_ui_helper.cc
new file mode 100644
index 0000000..22a1f6c
--- /dev/null
+++ b/chrome/browser/sync/sync_status_ui_helper.cc
@@ -0,0 +1,101 @@
+// Copyright (c) 2009 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#include "chrome/browser/sync/sync_status_ui_helper.h"
+
+#include "base/string_util.h"
+#include "chrome/browser/sync/auth_error_state.h"
+#include "chrome/browser/sync/personalization_strings.h"
+#include "chrome/browser/sync/profile_sync_service.h"
+
+static void GetLabelsForAuthError(AuthErrorState auth_error,
+ ProfileSyncService* service, std::wstring* status_label,
+ std::wstring* link_label) {
+ if (link_label)
+ link_label->assign(kSyncReLoginLinkLabel);
+ if (auth_error == AUTH_ERROR_INVALID_GAIA_CREDENTIALS) {
+ // If the user name is empty then the first login failed, otherwise the
+ // credentials are out-of-date.
+ if (service->GetAuthenticatedUsername().empty())
+ status_label->append(kSyncInvalidCredentialsError);
+ else
+ status_label->append(kSyncExpiredCredentialsError);
+ } else if (auth_error == AUTH_ERROR_CONNECTION_FAILED) {
+ // Note that there is little the user can do if the server is not
+ // reachable. Since attempting to re-connect is done automatically by
+ // the Syncer, we do not show the (re)login link.
+ status_label->append(kSyncServerUnavailableMsg);
+ if (link_label)
+ link_label->clear();
+ } else {
+ status_label->append(kSyncOtherLoginErrorLabel);
+ }
+}
+
+static std::wstring GetSyncedStateStatusLabel(ProfileSyncService* service) {
+ std::wstring label;
+ std::wstring user_name(UTF16ToWide(service->GetAuthenticatedUsername()));
+ if (user_name.empty())
+ return label;
+
+ label += kSyncAccountLabel;
+ label += user_name;
+ label += L"\n";
+ label += kLastSyncedLabel;
+ label += service->GetLastSyncedTimeString();
+ return label;
+}
+
+// static
+SyncStatusUIHelper::MessageType SyncStatusUIHelper::GetLabels(
+ ProfileSyncService* service, std::wstring* status_label,
+ std::wstring* link_label) {
+ MessageType result_type(SYNCED);
+ bool sync_enabled = service->IsSyncEnabledByUser();
+
+ if (sync_enabled) {
+ ProfileSyncService::Status status(service->QueryDetailedSyncStatus());
+ AuthErrorState auth_error(service->GetAuthErrorState());
+ // Either show auth error information with a link to re-login, auth in prog,
+ // or note that everything is OK with the last synced time.
+ status_label->assign(GetSyncedStateStatusLabel(service));
+ if (status.authenticated) {
+ // Everything is peachy.
+ DCHECK_EQ(auth_error, AUTH_ERROR_NONE);
+ } else if (service->UIShouldDepictAuthInProgress()) {
+ status_label->append(kSyncAuthenticatingLabel);
+ result_type = PRE_SYNCED;
+ } else if (auth_error != AUTH_ERROR_NONE) {
+ GetLabelsForAuthError(auth_error, service, status_label, link_label);
+ result_type = SYNC_ERROR;
+ } else {
+ NOTREACHED() << "Setup complete, backend !authenticated, AUTH_ERROR_NONE";
+ }
+ } else {
+ // Either show auth error information with a link to re-login, auth in prog,
+ // or provide a link to continue with setup.
+ result_type = PRE_SYNCED;
+ if (service->SetupInProgress()) {
+ ProfileSyncService::Status status(service->QueryDetailedSyncStatus());
+ AuthErrorState auth_error(service->GetAuthErrorState());
+ status_label->assign(UTF8ToWide(kSettingUpText));
+ if (service->UIShouldDepictAuthInProgress()) {
+ status_label->assign(kSyncAuthenticatingLabel);
+ } else if (auth_error != AUTH_ERROR_NONE) {
+ status_label->clear();
+ GetLabelsForAuthError(auth_error, service, status_label, NULL);
+ result_type = SYNC_ERROR;
+ } else if (!status.authenticated) {
+ status_label->assign(kSyncCredentialsNeededLabel);
+ }
+ } else {
+ status_label->assign(kSyncNotSetupInfo);
+ }
+ }
+ return result_type;
+}
+
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/sync/sync_status_ui_helper.h b/chrome/browser/sync/sync_status_ui_helper.h
new file mode 100644
index 0000000..6241ead
--- /dev/null
+++ b/chrome/browser/sync/sync_status_ui_helper.h
@@ -0,0 +1,34 @@
+// Copyright (c) 2009 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#ifndef CHROME_BROWSER_SYNC_SYNC_STATUS_UI_HELPER_H_
+#define CHROME_BROWSER_SYNC_SYNC_STATUS_UI_HELPER_H_
+
+#include "base/string16.h"
+
+class ProfileSyncService;
+
+// Utility to gather current sync status information from the sync service and
+// constructs messages suitable for showing in UI.
+class SyncStatusUIHelper {
+ public:
+ enum MessageType {
+ PRE_SYNCED, // User has not set up sync.
+ SYNCED, // We are synced and authenticated to a gmail account.
+ SYNC_ERROR, // A sync error (such as invalid credentials) has occurred.
+ };
+
+ // Create status and link labels for the current status labels and link text
+ // by querying |service|.
+ static MessageType GetLabels(ProfileSyncService* service,
+ std::wstring* status_label,
+ std::wstring* link_label);
+ private:
+ DISALLOW_IMPLICIT_CONSTRUCTORS(SyncStatusUIHelper);
+};
+
+#endif // CHROME_BROWSER_SYNC_SYNC_STATUS_UI_HELPER_H_
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/views/options/options_window_view.cc b/chrome/browser/views/options/options_window_view.cc
index ad77aa9..7554a57 100644
--- a/chrome/browser/views/options/options_window_view.cc
+++ b/chrome/browser/views/options/options_window_view.cc
@@ -11,13 +11,13 @@
#include "chrome/browser/views/options/advanced_page_view.h"
#include "chrome/browser/views/options/content_page_view.h"
#include "chrome/browser/views/options/general_page_view.h"
+#ifdef CHROME_PERSONALIZATION
+#include "chrome/browser/sync/personalization.h"
+#include "chrome/browser/views/options/user_data_page_view.h"
+#endif
#include "chrome/common/chrome_constants.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/pref_service.h"
-#ifdef CHROME_PERSONALIZATION
-#include "chrome/personalization/personalization.h"
-#include "chrome/personalization/views/user_data_page_view.h"
-#endif
#include "grit/chromium_strings.h"
#include "grit/generated_resources.h"
#include "grit/locale_settings.h"
diff --git a/chrome/browser/views/options/user_data_page_view.cc b/chrome/browser/views/options/user_data_page_view.cc
new file mode 100644
index 0000000..0dc6d95
--- /dev/null
+++ b/chrome/browser/views/options/user_data_page_view.cc
@@ -0,0 +1,186 @@
+// Copyright (c) 2006-2008 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#include "chrome/browser/views/user_data_page_view.h"
+
+#include "app/l10n_util.h"
+#include "base/path_service.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/profile_manager.h"
+#include "chrome/browser/sync/auth_error_state.h"
+#include "chrome/browser/sync/personalization_strings.h"
+#include "chrome/browser/sync/profile_sync_service.h"
+#include "chrome/browser/sync/sync_status_ui_helper.h"
+#include "chrome/browser/views/clear_browsing_data.h"
+#include "chrome/browser/views/importer_view.h"
+#include "chrome/browser/views/options/options_group_view.h"
+#include "chrome/browser/views/options/options_page_view.h"
+#include "chrome/common/chrome_paths.h"
+#include "chrome/common/pref_member.h"
+#include "chrome/common/pref_service.h"
+#include "grit/generated_resources.h"
+#include "third_party/skia/include/core/SkColor.h"
+#include "views/background.h"
+#include "views/controls/button/native_button.h"
+#include "views/controls/label.h"
+#include "views/controls/link.h"
+#include "views/controls/table/table_view.h"
+#include "views/grid_layout.h"
+#include "views/standard_layout.h"
+#include "views/view.h"
+#include "views/widget/widget.h"
+#include "views/window/window.h"
+
+// Background color for the status label when it's showing an error.
+static const SkColor kSyncLabelErrorBgColor = SkColorSetRGB(0xff, 0x9a, 0x9a);
+
+static views::Background* CreateErrorBackground() {
+ return views::Background::CreateSolidBackground(kSyncLabelErrorBgColor);
+}
+
+UserDataPageView::UserDataPageView(Profile* profile)
+ : OptionsPageView(profile),
+ sync_group_(NULL),
+ sync_status_label_(NULL),
+ sync_action_link_(NULL),
+ sync_start_stop_button_(NULL),
+ sync_service_(profile->GetProfilePersonalization()->sync_service()) {
+ DCHECK(sync_service_);
+ sync_service_->AddObserver(this);
+}
+
+UserDataPageView::~UserDataPageView() {
+ sync_service_->RemoveObserver(this);
+}
+
+void UserDataPageView::ButtonPressed(views::Button* sender) {
+ DCHECK(sender == sync_start_stop_button_);
+ if (!sync_service_->IsSyncEnabledByUser()) {
+ // Sync has not been enabled yet.
+ sync_service_->EnableForUser();
+ } else {
+ sync_service_->DisableForUser();
+ }
+}
+
+void UserDataPageView::LinkActivated(views::Link* source, int event_flags) {
+ DCHECK_EQ(source, sync_action_link_);
+ sync_service_->ShowLoginDialog();
+}
+
+void UserDataPageView::InitControlLayout() {
+ using views::GridLayout;
+ using views::ColumnSet;
+
+ GridLayout* layout = new GridLayout(this);
+ layout->SetInsets(5, 5, 5, 5);
+ SetLayoutManager(layout);
+
+ const int single_column_view_set_id = 0;
+ ColumnSet* column_set = layout->AddColumnSet(single_column_view_set_id);
+ column_set->AddColumn(GridLayout::FILL, GridLayout::FILL, 1,
+ GridLayout::USE_PREF, 0, 0);
+ layout->StartRow(0, single_column_view_set_id);
+ InitSyncGroup();
+ layout->AddView(sync_group_);
+ layout->AddPaddingRow(0, kRelatedControlVerticalSpacing);
+}
+
+void UserDataPageView::NotifyPrefChanged(const std::wstring* pref_name) {
+}
+
+void UserDataPageView::OnStateChanged() {
+ // If the UI controls are not yet initialized, then don't do anything. This
+ // can happen if the Options dialog is up, but the User Data tab is not yet
+ // clicked.
+ if (IsInitialized())
+ Layout();
+}
+
+void UserDataPageView::HighlightGroup(OptionsGroup highlight_group) {
+}
+
+void UserDataPageView::Layout() {
+ UpdateControls();
+ // We need to Layout twice - once to get the width of the contents box...
+ View::Layout();
+ int sync_group_width = sync_group_->GetContentsWidth();
+ sync_status_label_->SetBounds(0, 0, sync_group_width, 0);
+ // ... and twice to get the height of multi-line items correct.
+ View::Layout();
+}
+
+void UserDataPageView::InitSyncGroup() {
+ sync_status_label_ = new views::Label;
+ sync_status_label_->SetMultiLine(true);
+ sync_status_label_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
+
+ sync_action_link_ = new views::Link();
+ sync_action_link_->set_collapse_when_hidden(true);
+ sync_action_link_->SetController(this);
+
+ sync_start_stop_button_ = new views::NativeButton(this, std::wstring());
+
+ using views::GridLayout;
+ using views::ColumnSet;
+
+ views::View* contents = new views::View;
+ GridLayout* layout = new GridLayout(contents);
+ contents->SetLayoutManager(layout);
+
+ const int single_column_view_set_id = 0;
+ ColumnSet* column_set = layout->AddColumnSet(single_column_view_set_id);
+ column_set->AddColumn(GridLayout::LEADING, GridLayout::CENTER, 1,
+ GridLayout::USE_PREF, 0, 0);
+
+ // Currently we add the status label and the link in two rows. This is not
+ // the same as the mocks. If we add label and the link in two columns to make
+ // it look like the mocks then we can have a UI layout issue. If the label
+ // has text that fits in one line, the appearance is fine. But if the label
+ // has text with multiple lines, the appearance can be a bit detached. See
+ // bug 1648522 for details.
+ // TODO(munjal): Change the layout after we fix bug 1648522.
+ layout->StartRow(0, single_column_view_set_id);
+ layout->AddView(sync_status_label_);
+ layout->StartRow(0, single_column_view_set_id);
+ layout->AddView(sync_action_link_);
+ layout->AddPaddingRow(0, kRelatedControlVerticalSpacing);
+ layout->StartRow(0, single_column_view_set_id);
+ layout->AddView(sync_start_stop_button_);
+
+ sync_group_ = new OptionsGroupView(contents,
+ kSyncGroupName,
+ std::wstring(),
+ true);
+}
+
+void UserDataPageView::UpdateControls() {
+ std::wstring status_label;
+ std::wstring link_label;
+ std::wstring button_label;
+ bool sync_enabled = sync_service_->IsSyncEnabledByUser();
+ bool status_has_error = SyncStatusUIHelper::GetLabels(sync_service_,
+ &status_label, &link_label) == SyncStatusUIHelper::SYNC_ERROR;
+ button_label = sync_enabled ? kStopSyncButtonLabel :
+ sync_service_->SetupInProgress() ? UTF8ToWide(kSettingUpText)
+ : kStartSyncButtonLabel;
+
+ sync_status_label_->SetText(status_label);
+ sync_start_stop_button_->SetEnabled(!sync_service_->WizardIsVisible());
+ sync_start_stop_button_->SetLabel(button_label);
+ sync_action_link_->SetText(link_label);
+ sync_action_link_->SetVisible(!link_label.empty());
+ if (status_has_error) {
+ sync_status_label_->set_background(CreateErrorBackground());
+ sync_action_link_->set_background(CreateErrorBackground());
+ } else {
+ sync_status_label_->set_background(NULL);
+ sync_action_link_->set_background(NULL);
+ }
+}
+
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/views/options/user_data_page_view.h b/chrome/browser/views/options/user_data_page_view.h
new file mode 100644
index 0000000..840721a
--- /dev/null
+++ b/chrome/browser/views/options/user_data_page_view.h
@@ -0,0 +1,88 @@
+// Copyright (c) 2006-2008 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#ifndef CHROME_BROWSER_VIEWS_OPTIONS_USER_DATA_PAGE_VIEW_H_
+#define CHROME_BROWSER_VIEWS_OPTIONS_USER_DATA_PAGE_VIEW_H_
+
+#include "chrome/browser/sync/profile_sync_service.h"
+#include "chrome/browser/views/options/options_page_view.h"
+#include "chrome/common/pref_member.h"
+#include "views/controls/button/button.h"
+#include "views/controls/link.h"
+#include "views/view.h"
+
+namespace views {
+class GroupboxView;
+class Label;
+class NativeButton;
+}
+
+// TODO(idana): once the p13n module becomes public, we should get rid of the
+// sync specific options dialog tab and just add a bookmark sync section to the
+// existing (and newly added) "Personal Stuff" tab.
+
+class OptionsGroupView;
+
+///////////////////////////////////////////////////////////////////////////////
+// UserDataPageView
+
+class UserDataPageView : public OptionsPageView,
+ public views::ButtonListener,
+ public views::LinkController,
+ public ProfileSyncServiceObserver {
+ public:
+ explicit UserDataPageView(Profile* profile);
+ virtual ~UserDataPageView();
+
+ protected:
+ // views::ButtonListener implementation:
+ virtual void ButtonPressed(views::Button* sender);
+
+ // views::LinkController method.
+ virtual void LinkActivated(views::Link* source, int event_flags);
+
+ // OptionsPageView implementation:
+ virtual void InitControlLayout();
+ virtual void NotifyPrefChanged(const std::wstring* pref_name);
+ virtual void HighlightGroup(OptionsGroup highlight_group);
+
+ // views::View overrides:
+ virtual void Layout();
+
+ // ProfileSyncServiceObserver methods.
+ virtual void OnStateChanged();
+
+ private:
+ // Updates various controls based on the current sync state.
+ void UpdateControls();
+ // Returns whether initialization of controls is done or not.
+ bool IsInitialized() const {
+ // If initialization is already done, all the UI controls data members
+ // should be non-NULL. So check for one of them to determine if controls
+ // are already initialized or not.
+ return sync_group_ != NULL;
+ }
+ // Helper to get status label for synced state.
+ std::wstring GetSyncedStateStatusLabel() const;
+
+ void InitSyncGroup();
+
+ // Controls for the Sync group.
+ OptionsGroupView* sync_group_;
+ views::Label* sync_status_label_;
+ views::Link* sync_action_link_;
+ views::NativeButton* sync_start_stop_button_;
+
+ // Cached pointer to ProfileSyncService, if it exists. Kept up to date
+ // and NULL-ed out on destruction.
+ ProfileSyncService* sync_service_;
+
+ DISALLOW_COPY_AND_ASSIGN(UserDataPageView);
+};
+
+#endif // CHROME_BROWSER_VIEWS_OPTIONS_USER_DATA_PAGE_VIEW_H_
+
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/views/sync/sync_setup_flow.cc b/chrome/browser/views/sync/sync_setup_flow.cc
new file mode 100644
index 0000000..5df1a33
--- /dev/null
+++ b/chrome/browser/views/sync/sync_setup_flow.cc
@@ -0,0 +1,240 @@
+// Copyright (c) 2009 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#include "base/json_reader.h"
+#include "base/json_writer.h"
+#include "base/string_util.h"
+#include "base/values.h"
+#include "chrome/browser/browser.h"
+#include "chrome/browser/browser_list.h"
+#include "chrome/browser/renderer_host/render_view_host.h"
+#include "chrome/browser/sync/auth_error_state.h"
+#include "chrome/browser/sync/profile_sync_service.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/views/sync/sync_setup_flow.h"
+#include "chrome/common/pref_service.h"
+
+static const int kSyncDialogWidth = 267;
+static const int kSyncDialogHeight = 369;
+
+// XPath expression for finding specific iframes.
+static const wchar_t* kLoginIFrameXPath = L"//iframe[@id='login']";
+static const wchar_t* kMergeIFrameXPath = L"//iframe[@id='merge']";
+
+// Helper function to read the JSON string from the Value parameter.
+static std::string GetJsonResponse(const Value* content) {
+ if (!content || !content->IsType(Value::TYPE_LIST)) {
+ NOTREACHED();
+ return std::string();
+ }
+ const ListValue* args = static_cast<const ListValue*>(content);
+ if (args->GetSize() != 1) {
+ NOTREACHED();
+ return std::string();
+ }
+
+ std::string result;
+ Value* value = NULL;
+ if (!args->Get(0, &value) || !value->GetAsString(&result)) {
+ NOTREACHED();
+ return std::string();
+ }
+
+ return result;
+}
+
+void FlowHandler::RegisterMessages() {
+ dom_ui_->RegisterMessageCallback("SubmitAuth",
+ NewCallback(this, &FlowHandler::HandleSubmitAuth));
+ dom_ui_->RegisterMessageCallback("SubmitMergeAndSync",
+ NewCallback(this, &FlowHandler::HandleSubmitMergeAndSync));
+}
+
+static bool GetUsernameAndPassword(const std::string& json,
+ std::string* username, std::string* password) {
+ scoped_ptr<Value> parsed_value(JSONReader::Read(json, false));
+ if (!parsed_value.get() || !parsed_value->IsType(Value::TYPE_DICTIONARY))
+ return false;
+
+ DictionaryValue* result = static_cast<DictionaryValue*>(parsed_value.get());
+ if (!result->GetString(L"user", username) ||
+ !result->GetString(L"pass", password)) {
+ return false;
+ }
+ return true;
+}
+
+void FlowHandler::HandleSubmitAuth(const Value* value) {
+ std::string json(GetJsonResponse(value));
+ std::string username, password;
+ if (json.empty())
+ return;
+
+ if (!GetUsernameAndPassword(json, &username, &password)) {
+ // The page sent us something that we didn't understand.
+ // This probably indicates a programming error.
+ NOTREACHED();
+ return;
+ }
+
+ if (flow_)
+ flow_->OnUserSubmittedAuth(username, password);
+}
+
+void FlowHandler::HandleSubmitMergeAndSync(const Value* value) {
+ if (flow_)
+ flow_->OnUserAcceptedMergeAndSync();
+}
+
+// Called by SyncSetupFlow::Advance.
+void FlowHandler::ShowGaiaLogin(const DictionaryValue& args) {
+ std::string json;
+ JSONWriter::Write(&args, false, &json);
+ std::wstring javascript = std::wstring(L"showGaiaLogin") +
+ L"(" + UTF8ToWide(json) + L");";
+ ExecuteJavascriptInIFrame(kLoginIFrameXPath, javascript);
+}
+
+void FlowHandler::ShowGaiaSuccessAndClose() {
+ ExecuteJavascriptInIFrame(kLoginIFrameXPath, L"showGaiaSuccessAndClose();");
+}
+
+void FlowHandler::ShowGaiaSuccessAndSettingUp() {
+ ExecuteJavascriptInIFrame(kLoginIFrameXPath,
+ L"showGaiaSuccessAndSettingUp();");
+}
+
+void FlowHandler::ShowMergeAndSync() {
+ if (dom_ui_) // NULL during testing.
+ dom_ui_->CallJavascriptFunction(L"showMergeAndSync");
+}
+
+void FlowHandler::ShowMergeAndSyncDone() {
+ ExecuteJavascriptInIFrame(kMergeIFrameXPath, L"showMergeAndSyncDone();");
+}
+
+void FlowHandler::ExecuteJavascriptInIFrame(const std::wstring& iframe_xpath,
+ const std::wstring& js) {
+ if (dom_ui_) {
+ RenderViewHost* rvh = dom_ui_->tab_contents()->render_view_host();
+ rvh->ExecuteJavascriptInWebFrame(iframe_xpath, js);
+ }
+}
+
+SyncSetupFlow::~SyncSetupFlow() {
+ flow_handler_->set_flow(NULL);
+}
+
+void SyncSetupFlow::GetDialogSize(gfx::Size* size) const {
+ size->set_width(kSyncDialogWidth);
+ size->set_height(kSyncDialogHeight);
+}
+
+// A callback to notify the delegate that the dialog closed.
+void SyncSetupFlow::OnDialogClosed(const std::string& json_retval) {
+ DCHECK(json_retval.empty());
+ container_->set_flow(NULL); // Sever ties from the wizard.
+ if (current_state_ == SyncSetupWizard::DONE) {
+ PrefService* prefs = service_->profile()->GetPrefs();
+ prefs->SetBoolean(prefs::kSyncHasSetupCompleted, true);
+ prefs->ScheduleSavePersistentPrefs();
+ }
+ service_->OnUserCancelledDialog();
+ delete this;
+}
+
+// static
+void SyncSetupFlow::GetArgsForGaiaLogin(const ProfileSyncService* service,
+ DictionaryValue* args) {
+ AuthErrorState error(service->GetAuthErrorState());
+ if (!service->last_attempted_user_email().empty()) {
+ args->SetString(L"user", service->last_attempted_user_email());
+ args->SetInteger(L"error", error);
+ } else {
+ std::wstring user(UTF16ToWide(service->GetAuthenticatedUsername()));
+ args->SetString(L"user", user);
+ args->SetInteger(L"error", user.empty() ? 0 : error);
+ }
+}
+
+void SyncSetupFlow::GetDOMMessageHandlers(
+ std::vector<DOMMessageHandler*>* handlers) const {
+ handlers->push_back(flow_handler_);
+}
+
+bool SyncSetupFlow::ShouldAdvance(SyncSetupWizard::State state) {
+ switch (state) {
+ case SyncSetupWizard::GAIA_LOGIN:
+ return current_state_ == SyncSetupWizard::GAIA_LOGIN;
+ case SyncSetupWizard::GAIA_SUCCESS:
+ return current_state_ == SyncSetupWizard::GAIA_LOGIN;
+ case SyncSetupWizard::MERGE_AND_SYNC:
+ return current_state_ == SyncSetupWizard::GAIA_SUCCESS;
+ case SyncSetupWizard::DONE:
+ return current_state_ == SyncSetupWizard::MERGE_AND_SYNC ||
+ current_state_ == SyncSetupWizard::GAIA_SUCCESS;
+ default:
+ NOTREACHED() << "Unhandled State: " << state;
+ return false;
+ }
+}
+
+void SyncSetupFlow::Advance(SyncSetupWizard::State advance_state) {
+ if (!ShouldAdvance(advance_state))
+ return;
+ switch (advance_state) {
+ case SyncSetupWizard::GAIA_LOGIN: {
+ DictionaryValue args;
+ SyncSetupFlow::GetArgsForGaiaLogin(service_, &args);
+ flow_handler_->ShowGaiaLogin(args);
+ break;
+ }
+ case SyncSetupWizard::GAIA_SUCCESS:
+ if (end_state_ == SyncSetupWizard::GAIA_SUCCESS)
+ flow_handler_->ShowGaiaSuccessAndClose();
+ else
+ flow_handler_->ShowGaiaSuccessAndSettingUp();
+ break;
+ case SyncSetupWizard::MERGE_AND_SYNC:
+ flow_handler_->ShowMergeAndSync();
+ break;
+ case SyncSetupWizard::DONE:
+ if (current_state_ == SyncSetupWizard::MERGE_AND_SYNC)
+ flow_handler_->ShowMergeAndSyncDone();
+ else if (current_state_ == SyncSetupWizard::GAIA_SUCCESS)
+ flow_handler_->ShowGaiaSuccessAndClose();
+ break;
+ default:
+ NOTREACHED() << "Invalid advance state: " << advance_state;
+ }
+ current_state_ = advance_state;
+}
+
+
+// static
+SyncSetupFlow* SyncSetupFlow::Run(ProfileSyncService* service,
+ SyncSetupFlowContainer* container,
+ SyncSetupWizard::State start,
+ SyncSetupWizard::State end) {
+ DictionaryValue args;
+ if (start == SyncSetupWizard::GAIA_LOGIN)
+ SyncSetupFlow::GetArgsForGaiaLogin(service, &args);
+ std::string json_args;
+ JSONWriter::Write(&args, false, &json_args);
+
+ Browser* b = BrowserList::GetLastActive();
+ if (!b)
+ return NULL;
+
+ FlowHandler* handler = new FlowHandler();
+ SyncSetupFlow* flow = new SyncSetupFlow(start, end, json_args,
+ container, handler, service);
+ handler->set_flow(flow);
+ b->BrowserShowHtmlDialog(flow, NULL);
+ return flow;
+}
+
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/views/sync/sync_setup_flow.h b/chrome/browser/views/sync/sync_setup_flow.h
new file mode 100644
index 0000000..ff86f7b
--- /dev/null
+++ b/chrome/browser/views/sync/sync_setup_flow.h
@@ -0,0 +1,176 @@
+// Copyright (c) 2006-2008 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#ifndef CHROME_BROWSER_VIEWS_SYNC_SYNC_SETUP_FLOW_H_
+#define CHROME_BROWSER_VIEWS_SYNC_SYNC_SETUP_FLOW_H_
+
+#include <string>
+#include <vector>
+
+#include "chrome/browser/dom_ui/html_dialog_ui.h"
+#include "chrome/browser/sync/personalization_strings.h"
+#include "chrome/browser/views/sync/sync_setup_wizard.h"
+
+class FlowHandler;
+class SyncSetupFlowContainer;
+
+// The state machine used by SyncSetupWizard, exposed in its own header
+// to facilitate testing of SyncSetupWizard. This class is used to open and
+// run the html dialog and deletes itself when the dialog closes.
+class SyncSetupFlow : public HtmlDialogUIDelegate {
+ public:
+ virtual ~SyncSetupFlow();
+
+ // Runs a flow from |start| to |end|, and does the work of actually showing
+ // the HTML dialog. |container| is kept up-to-date with the lifetime of the
+ // flow (e.g it is emptied on dialog close).
+ static SyncSetupFlow* Run(ProfileSyncService* service,
+ SyncSetupFlowContainer* container,
+ SyncSetupWizard::State start,
+ SyncSetupWizard::State end);
+
+ // Fills |args| with "user" and "error" arguments by querying |service|.
+ static void GetArgsForGaiaLogin(
+ const ProfileSyncService* service,
+ DictionaryValue* args);
+
+ // Triggers a state machine transition to advance_state.
+ void Advance(SyncSetupWizard::State advance_state);
+
+ // HtmlDialogUIDelegate implementation.
+ // Get the HTML file path for the content to load in the dialog.
+ virtual GURL GetDialogContentURL() const {
+ return GURL("cloudy://resources/setup");
+ }
+
+ // HtmlDialogUIDelegate implementation.
+ virtual void GetDOMMessageHandlers(
+ std::vector<DOMMessageHandler*>* handlers) const;
+
+ // HtmlDialogUIDelegate implementation.
+ // Get the size of the dialog.
+ virtual void GetDialogSize(gfx::Size* size) const;
+
+ // HtmlDialogUIDelegate implementation.
+ // Gets the JSON string input to use when opening the dialog.
+ virtual std::string GetDialogArgs() const {
+ return dialog_start_args_;
+ }
+
+ // HtmlDialogUIDelegate implementation.
+ // A callback to notify the delegate that the dialog closed.
+ virtual void OnDialogClosed(const std::string& json_retval);
+
+ // HtmlDialogUIDelegate implementation.
+ virtual std::wstring GetDialogTitle() const {
+ return kLoginDialogTitle;
+ }
+
+ // HtmlDialogUIDelegate implementation.
+ virtual bool IsDialogModal() const {
+ return false;
+ }
+
+ void OnUserSubmittedAuth(const std::string& username,
+ const std::string& password) {
+ service_->OnUserSubmittedAuth(username, password);
+ }
+
+ void OnUserAcceptedMergeAndSync() {
+ service_->OnUserAcceptedMergeAndSync();
+ }
+
+ private:
+ FRIEND_TEST(SyncSetupWizardTest, InitialStepLogin);
+ FRIEND_TEST(SyncSetupWizardTest, InitialStepMergeAndSync);
+ FRIEND_TEST(SyncSetupWizardTest, DialogCancelled);
+ FRIEND_TEST(SyncSetupWizardTest, InvalidTransitions);
+ FRIEND_TEST(SyncSetupWizardTest, FullSuccessfulRunSetsPref);
+ FRIEND_TEST(SyncSetupWizardTest, DiscreteRun);
+
+ // Use static Run method to get an instance.
+ SyncSetupFlow(SyncSetupWizard::State start_state,
+ SyncSetupWizard::State end_state,
+ const std::string& args, SyncSetupFlowContainer* container,
+ FlowHandler* handler, ProfileSyncService* service)
+ : container_(container), dialog_start_args_(args),
+ current_state_(start_state), end_state_(end_state),
+ flow_handler_(handler), service_(service) {
+ }
+
+ // Returns true if |this| should transition its state machine to |state|
+ // based on |current_state_|, or false if that would be nonsense or is
+ // a no-op.
+ bool ShouldAdvance(SyncSetupWizard::State state);
+
+ SyncSetupFlowContainer* container_; // Our container. Don't own this.
+ std::string dialog_start_args_; // The args to pass to the initial page.
+
+ SyncSetupWizard::State current_state_;
+ SyncSetupWizard::State end_state_; // The goal.
+
+ // The handler needed for the entire flow. We don't own this.
+ FlowHandler* flow_handler_;
+
+ // We need this to write the sentinel "setup completed" pref.
+ ProfileSyncService* service_;
+
+ DISALLOW_COPY_AND_ASSIGN(SyncSetupFlow);
+};
+
+// A really simple wrapper for a SyncSetupFlow so that we don't have to
+// add any public methods to the public SyncSetupWizard interface to notify it
+// when the dialog closes.
+class SyncSetupFlowContainer {
+ public:
+ SyncSetupFlowContainer() : flow_(NULL) { }
+ void set_flow(SyncSetupFlow* flow) {
+ DCHECK(!flow_ || !flow);
+ flow_ = flow;
+ }
+
+ SyncSetupFlow* get_flow() { return flow_; }
+ private:
+ SyncSetupFlow* flow_;
+
+ DISALLOW_COPY_AND_ASSIGN(SyncSetupFlowContainer);
+};
+
+// The FlowHandler connects the state machine to the dialog backing HTML and
+// JS namespace by implementing DOMMessageHandler and being invoked by the
+// SyncSetupFlow. Exposed here to facilitate testing.
+class FlowHandler : public DOMMessageHandler {
+ public:
+ FlowHandler() {}
+ virtual ~FlowHandler() {}
+
+ // DOMMessageHandler implementation.
+ virtual void RegisterMessages();
+
+ // Callbacks from the page.
+ void HandleSubmitAuth(const Value* value);
+ void HandleSubmitMergeAndSync(const Value* value);
+
+ // These functions control which part of the HTML is visible.
+ void ShowGaiaLogin(const DictionaryValue& args);
+ void ShowGaiaSuccessAndClose();
+ void ShowGaiaSuccessAndSettingUp();
+ void ShowMergeAndSync();
+ void ShowMergeAndSyncDone();
+
+ void set_flow(SyncSetupFlow* flow) {
+ flow_ = flow;
+ }
+
+ private:
+ void ExecuteJavascriptInIFrame(const std::wstring& iframe_xpath,
+ const std::wstring& js);
+ SyncSetupFlow* flow_;
+ DISALLOW_COPY_AND_ASSIGN(FlowHandler);
+};
+
+#endif // CHROME_BROWSER_VIEWS_SYNC_SYNC_SETUP_FLOW_H_
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/views/sync/sync_setup_wizard.cc b/chrome/browser/views/sync/sync_setup_wizard.cc
new file mode 100644
index 0000000..18ef7b8
--- /dev/null
+++ b/chrome/browser/views/sync/sync_setup_wizard.cc
@@ -0,0 +1,57 @@
+// Copyright (c) 2006-2008 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.
+
+#ifdef CHROME_PERSONALIZATION
+
+#include "chrome/browser/views/sync/sync_setup_wizard.h"
+
+#include "chrome/common/pref_service.h"
+#include "chrome/browser/sync/profile_sync_service.h"
+#include "chrome/browser/views/sync/sync_setup_flow.h"
+
+SyncSetupWizard::SyncSetupWizard(ProfileSyncService* service)
+ : service_(service), flow_container_(new SyncSetupFlowContainer()) {
+}
+
+SyncSetupWizard::~SyncSetupWizard() {
+ delete flow_container_;
+}
+
+void SyncSetupWizard::Step(State advance_state) {
+ SyncSetupFlow* flow = flow_container_->get_flow();
+ if (flow) {
+ // A setup flow is in progress and dialog is currently showing.
+ flow->Advance(advance_state);
+ } else if (!service_->profile()->GetPrefs()->GetBoolean(
+ prefs::kSyncHasSetupCompleted)) {
+ if (advance_state == DONE || advance_state == GAIA_SUCCESS)
+ return;
+ // No flow is in progress, and we have never escorted the user all the
+ // way through the wizard flow.
+ flow_container_->set_flow(
+ SyncSetupFlow::Run(service_, flow_container_, advance_state, DONE));
+ } else {
+ // No flow in in progress, but we've finished the wizard flow once before.
+ // This is just a discrete run.
+ if (advance_state == DONE || advance_state == GAIA_SUCCESS)
+ return; // Nothing to do.
+ flow_container_->set_flow(SyncSetupFlow::Run(service_, flow_container_,
+ advance_state, GetEndStateForDiscreteRun(advance_state)));
+ }
+}
+
+bool SyncSetupWizard::IsVisible() const {
+ return flow_container_->get_flow() != NULL;
+}
+
+// static
+SyncSetupWizard::State SyncSetupWizard::GetEndStateForDiscreteRun(
+ State start_state) {
+ State result = start_state == GAIA_LOGIN ? GAIA_SUCCESS : DONE;
+ DCHECK_NE(DONE, result) <<
+ "Invalid start state for discrete run: " << start_state;
+ return result;
+}
+
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/views/sync/sync_setup_wizard.h b/chrome/browser/views/sync/sync_setup_wizard.h
new file mode 100644
index 0000000..7de7253
--- /dev/null
+++ b/chrome/browser/views/sync/sync_setup_wizard.h
@@ -0,0 +1,50 @@
+// Copyright (c) 2006-2008 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.
+
+#ifndef CHROME_BROWSER_VIEWS_SYNC_SYNC_SETUP_WIZARD_H_
+#define CHROME_BROWSER_VIEWS_SYNC_SYNC_SETUP_WIZARD_H_
+
+#include "base/basictypes.h"
+
+class SyncSetupFlowContainer;
+
+class ProfileSyncService;
+
+class SyncSetupWizard {
+ public:
+ enum State {
+ GAIA_LOGIN = 0,
+ GAIA_SUCCESS,
+ MERGE_AND_SYNC,
+ DONE
+ };
+
+ explicit SyncSetupWizard(ProfileSyncService* service);
+ ~SyncSetupWizard();
+
+ // Advances the wizard to the specified state if possible, or opens a
+ // new dialog starting at |advance_state|. If the wizard has never ran
+ // through to completion, it will always attempt to do so. Otherwise, e.g
+ // for a transient auth failure, it will just run as far as is necessary
+ // based on |advance_state| (so for auth failure, up to GAIA_SUCCESS).
+ void Step(State advance_state);
+
+ // Whether or not a dialog is currently showing. Useful to determine
+ // if various buttons in the UI should be enabled or disabled.
+ bool IsVisible() const;
+
+ private:
+ // If we just need to pop open an individual dialog, say to collect
+ // gaia credentials in the event of a steady-state auth failure, this is
+ // a "discrete" run (as in not a continuous wizard flow). This returns
+ // the end state to pass to Run for a given |start_state|.
+ static State GetEndStateForDiscreteRun(State start_state);
+
+ ProfileSyncService* service_;
+ SyncSetupFlowContainer* flow_container_;
+
+ DISALLOW_COPY_AND_ASSIGN(SyncSetupWizard);
+};
+
+#endif // CHROME_BROWSER_VIEWS_SYNC_SYNC_SETUP_WIZARD_H_
diff --git a/chrome/browser/views/sync/sync_setup_wizard_unittest.cc b/chrome/browser/views/sync/sync_setup_wizard_unittest.cc
new file mode 100644
index 0000000..bfdfd73
--- /dev/null
+++ b/chrome/browser/views/sync/sync_setup_wizard_unittest.cc
@@ -0,0 +1,329 @@
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifdef CHROME_PERSONALIZATION
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+#include "base/json_writer.h"
+#include "chrome/browser/browser.h"
+#include "chrome/browser/browser_list.h"
+#include "chrome/browser/sync/personalization.h"
+#include "chrome/browser/sync/profile_sync_service.h"
+#include "chrome/browser/views/sync/sync_setup_flow.h"
+#include "chrome/browser/views/sync/sync_setup_wizard.h"
+#include "chrome/test/browser_with_test_window_test.h"
+#include "chrome/test/testing_profile.h"
+#include "chrome/test/test_browser_window.h"
+
+static const char* kTestUser = "chrome.p13n.test@gmail.com";
+static const char* kTestPassword = "g00gl3g00gl3";
+
+// A PSS subtype to inject.
+class ProfileSyncServiceForWizardTest : public ProfileSyncService {
+ public:
+ explicit ProfileSyncServiceForWizardTest(Profile* profile)
+ : ProfileSyncService(profile), user_accepted_merge_and_sync_(false),
+ user_cancelled_dialog_(false) {
+ }
+
+ virtual ~ProfileSyncServiceForWizardTest() { }
+
+ virtual void OnUserSubmittedAuth(const std::string& username,
+ const std::string& password) {
+ username_ = username;
+ password_ = password;
+ }
+ virtual void OnUserAcceptedMergeAndSync() {
+ user_accepted_merge_and_sync_ = true;
+ }
+ virtual void OnUserCancelledDialog() {
+ user_cancelled_dialog_ = true;
+ }
+
+ virtual string16 GetAuthenticatedUsername() const {
+ return UTF8ToWide(username_);
+ }
+
+ void set_auth_state(const std::string& last_email, AuthErrorState state) {
+ last_attempted_user_email_ = last_email;
+ last_auth_error_ = state;
+ }
+
+ void ResetTestStats() {
+ username_.clear();
+ password_.clear();
+ user_accepted_merge_and_sync_ = false;
+ user_cancelled_dialog_ = false;
+ }
+
+ std::string username_;
+ std::string password_;
+ bool user_accepted_merge_and_sync_;
+ bool user_cancelled_dialog_;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(ProfileSyncServiceForWizardTest);
+};
+
+class ProfilePersonalizationTestImpl : public ProfilePersonalization {
+ public:
+ explicit ProfilePersonalizationTestImpl(Profile* p)
+ : sync_service_(new ProfileSyncServiceForWizardTest(p)) {
+ }
+ virtual ProfileSyncService* sync_service() { return sync_service_.get(); }
+ private:
+ scoped_ptr<ProfileSyncService> sync_service_;
+};
+
+class TestingProfileWithPersonalization : public TestingProfile {
+ public:
+ TestingProfileWithPersonalization() {
+ personalization_.reset(new ProfilePersonalizationTestImpl(this));
+ }
+
+ virtual ProfilePersonalization* GetProfilePersonalization() {
+ return personalization_.get();
+ }
+ private:
+ scoped_ptr<ProfilePersonalization> personalization_;
+};
+
+class TestBrowserWindowForWizardTest : public TestBrowserWindow {
+ public:
+ explicit TestBrowserWindowForWizardTest(Browser* browser)
+ : TestBrowserWindow(browser), flow_(NULL),
+ was_show_html_dialog_called_(false) {
+ }
+
+ // We intercept this call to hijack the flow created and then use it to
+ // drive the wizard as if we were the HTML page.
+ virtual void ShowHTMLDialog(HtmlDialogUIDelegate* delegate,
+ gfx::NativeWindow parent_window) {
+ flow_ = static_cast<SyncSetupFlow*>(delegate);
+ was_show_html_dialog_called_ = true;
+ }
+
+ bool TestAndResetWasShowHTMLDialogCalled() {
+ bool ret = was_show_html_dialog_called_;
+ was_show_html_dialog_called_ = false;
+ return ret;
+ }
+
+ SyncSetupFlow* flow_;
+ private:
+ bool was_show_html_dialog_called_;
+};
+
+class SyncSetupWizardTest : public BrowserWithTestWindowTest {
+ public:
+ SyncSetupWizardTest() : test_window_(NULL), wizard_(NULL) { }
+ virtual ~SyncSetupWizardTest() { }
+ virtual void SetUp() {
+ set_profile(new TestingProfileWithPersonalization());
+ profile()->CreateBookmarkModel(false);
+ // Wait for the bookmarks model to load.
+ profile()->BlockUntilBookmarkModelLoaded();
+ profile()->GetPrefs()->RegisterBooleanPref(prefs::kSyncHasSetupCompleted,
+ false);
+ set_browser(new Browser(Browser::TYPE_NORMAL, profile()));
+ test_window_ = new TestBrowserWindowForWizardTest(browser());
+ set_window(test_window_);
+ browser()->set_window(window());
+ BrowserList::SetLastActive(browser());
+ service_ = static_cast<ProfileSyncServiceForWizardTest*>(
+ profile()->GetProfilePersonalization()->sync_service());
+ wizard_.reset(new SyncSetupWizard(service_));
+ }
+
+ virtual void TearDown() {
+ test_window_ = NULL;
+ service_ = NULL;
+ wizard_.reset();
+ }
+
+ TestBrowserWindowForWizardTest* test_window_;
+ scoped_ptr<SyncSetupWizard> wizard_;
+ ProfileSyncServiceForWizardTest* service_;
+};
+
+TEST_F(SyncSetupWizardTest, InitialStepLogin) {
+ DictionaryValue dialog_args;
+ SyncSetupFlow::GetArgsForGaiaLogin(service_, &dialog_args);
+ std::string json_start_args;
+ JSONWriter::Write(&dialog_args, false, &json_start_args);
+ ListValue credentials;
+ std::string auth = "{\"user\":\"";
+ auth += std::string(kTestUser) + "\",\"pass\":\"";
+ auth += std::string(kTestPassword) + "\"}";
+ credentials.Append(new StringValue(auth));
+
+ EXPECT_FALSE(wizard_->IsVisible());
+ EXPECT_FALSE(test_window_->flow_);
+ wizard_->Step(SyncSetupWizard::GAIA_LOGIN);
+
+ EXPECT_TRUE(wizard_->IsVisible());
+ EXPECT_TRUE(test_window_->TestAndResetWasShowHTMLDialogCalled());
+ EXPECT_EQ(SyncSetupWizard::GAIA_LOGIN, test_window_->flow_->current_state_);
+ EXPECT_EQ(SyncSetupWizard::DONE, test_window_->flow_->end_state_);
+ EXPECT_EQ(json_start_args, test_window_->flow_->dialog_start_args_);
+
+ // Simulate the user submitting credentials.
+ test_window_->flow_->flow_handler_->HandleSubmitAuth(&credentials);
+ EXPECT_TRUE(wizard_->IsVisible());
+ EXPECT_EQ(SyncSetupWizard::GAIA_LOGIN, test_window_->flow_->current_state_);
+ EXPECT_EQ(kTestUser, service_->username_);
+ EXPECT_EQ(kTestPassword, service_->password_);
+ EXPECT_FALSE(service_->user_accepted_merge_and_sync_);
+ EXPECT_FALSE(service_->user_cancelled_dialog_);
+ service_->ResetTestStats();
+
+ // Simulate failed credentials.
+ service_->set_auth_state(kTestUser, AUTH_ERROR_INVALID_GAIA_CREDENTIALS);
+ wizard_->Step(SyncSetupWizard::GAIA_LOGIN);
+ EXPECT_TRUE(wizard_->IsVisible());
+ EXPECT_FALSE(test_window_->TestAndResetWasShowHTMLDialogCalled());
+ EXPECT_EQ(SyncSetupWizard::GAIA_LOGIN, test_window_->flow_->current_state_);
+ dialog_args.Clear();
+ SyncSetupFlow::GetArgsForGaiaLogin(service_, &dialog_args);
+ EXPECT_EQ(2, dialog_args.GetSize());
+ std::string actual_user;
+ dialog_args.GetString(L"user", &actual_user);
+ EXPECT_EQ(kTestUser, actual_user);
+ int error = -1;
+ dialog_args.GetInteger(L"error", &error);
+ EXPECT_EQ(static_cast<int>(AUTH_ERROR_INVALID_GAIA_CREDENTIALS), error);
+ service_->set_auth_state(kTestUser, AUTH_ERROR_NONE);
+
+ // Simulate success.
+ wizard_->Step(SyncSetupWizard::GAIA_SUCCESS);
+ EXPECT_TRUE(wizard_->IsVisible());
+ EXPECT_FALSE(test_window_->TestAndResetWasShowHTMLDialogCalled());
+ EXPECT_EQ(SyncSetupWizard::GAIA_SUCCESS, test_window_->flow_->current_state_);
+
+ wizard_->Step(SyncSetupWizard::DONE); // No merge and sync.
+ EXPECT_TRUE(wizard_->IsVisible());
+ EXPECT_FALSE(test_window_->TestAndResetWasShowHTMLDialogCalled());
+ EXPECT_EQ(SyncSetupWizard::DONE, test_window_->flow_->current_state_);
+}
+
+TEST_F(SyncSetupWizardTest, InitialStepMergeAndSync) {
+ wizard_->Step(SyncSetupWizard::GAIA_LOGIN);
+ EXPECT_TRUE(wizard_->IsVisible());
+ EXPECT_TRUE(test_window_->TestAndResetWasShowHTMLDialogCalled());
+ EXPECT_EQ(SyncSetupWizard::DONE, test_window_->flow_->end_state_);
+
+ wizard_->Step(SyncSetupWizard::GAIA_SUCCESS);
+ wizard_->Step(SyncSetupWizard::MERGE_AND_SYNC);
+ EXPECT_TRUE(wizard_->IsVisible());
+ EXPECT_FALSE(test_window_->TestAndResetWasShowHTMLDialogCalled());
+ EXPECT_EQ(SyncSetupWizard::MERGE_AND_SYNC,
+ test_window_->flow_->current_state_);
+
+ test_window_->flow_->flow_handler_->HandleSubmitMergeAndSync(NULL);
+ EXPECT_TRUE(wizard_->IsVisible());
+ EXPECT_EQ(SyncSetupWizard::MERGE_AND_SYNC,
+ test_window_->flow_->current_state_);
+ EXPECT_EQ(std::string(), service_->username_);
+ EXPECT_EQ(std::string(), service_->password_);
+ EXPECT_TRUE(service_->user_accepted_merge_and_sync_);
+ EXPECT_FALSE(service_->user_cancelled_dialog_);
+ service_->ResetTestStats();
+ wizard_->Step(SyncSetupWizard::DONE); // No merge and sync.
+ EXPECT_TRUE(wizard_->IsVisible());
+ EXPECT_FALSE(test_window_->TestAndResetWasShowHTMLDialogCalled());
+ EXPECT_EQ(SyncSetupWizard::DONE, test_window_->flow_->current_state_);
+}
+
+TEST_F(SyncSetupWizardTest, DialogCancelled) {
+ wizard_->Step(SyncSetupWizard::GAIA_LOGIN);
+ test_window_->flow_->OnDialogClosed("");
+ EXPECT_FALSE(wizard_->IsVisible());
+ EXPECT_TRUE(service_->user_cancelled_dialog_);
+ EXPECT_EQ(std::string(), service_->username_);
+ EXPECT_EQ(std::string(), service_->password_);
+ EXPECT_FALSE(service_->user_accepted_merge_and_sync_);
+
+ wizard_->Step(SyncSetupWizard::GAIA_LOGIN);
+ EXPECT_TRUE(wizard_->IsVisible());
+ EXPECT_TRUE(test_window_->TestAndResetWasShowHTMLDialogCalled());
+ wizard_->Step(SyncSetupWizard::GAIA_LOGIN);
+ EXPECT_FALSE(test_window_->TestAndResetWasShowHTMLDialogCalled());
+
+ wizard_->Step(SyncSetupWizard::MERGE_AND_SYNC);
+ test_window_->flow_->OnDialogClosed("");
+ EXPECT_FALSE(wizard_->IsVisible());
+ EXPECT_TRUE(service_->user_cancelled_dialog_);
+ EXPECT_EQ(std::string(), service_->username_);
+ EXPECT_EQ(std::string(), service_->password_);
+ EXPECT_FALSE(service_->user_accepted_merge_and_sync_);
+}
+
+TEST_F(SyncSetupWizardTest, InvalidTransitions) {
+ wizard_->Step(SyncSetupWizard::GAIA_SUCCESS);
+ EXPECT_FALSE(wizard_->IsVisible());
+ EXPECT_FALSE(test_window_->TestAndResetWasShowHTMLDialogCalled());
+
+ wizard_->Step(SyncSetupWizard::DONE);
+ EXPECT_FALSE(wizard_->IsVisible());
+ EXPECT_FALSE(test_window_->TestAndResetWasShowHTMLDialogCalled());
+
+ wizard_->Step(SyncSetupWizard::GAIA_LOGIN);
+ wizard_->Step(SyncSetupWizard::MERGE_AND_SYNC);
+ EXPECT_EQ(SyncSetupWizard::GAIA_LOGIN, test_window_->flow_->current_state_);
+
+ wizard_->Step(SyncSetupWizard::DONE);
+ EXPECT_EQ(SyncSetupWizard::GAIA_LOGIN, test_window_->flow_->current_state_);
+
+ wizard_->Step(SyncSetupWizard::GAIA_SUCCESS);
+ wizard_->Step(SyncSetupWizard::MERGE_AND_SYNC);
+ EXPECT_EQ(SyncSetupWizard::MERGE_AND_SYNC,
+ test_window_->flow_->current_state_);
+
+ wizard_->Step(SyncSetupWizard::GAIA_SUCCESS);
+ EXPECT_EQ(SyncSetupWizard::MERGE_AND_SYNC,
+ test_window_->flow_->current_state_);
+}
+
+TEST_F(SyncSetupWizardTest, FullSuccessfulRunSetsPref) {
+ wizard_->Step(SyncSetupWizard::GAIA_LOGIN);
+ wizard_->Step(SyncSetupWizard::GAIA_SUCCESS);
+ wizard_->Step(SyncSetupWizard::MERGE_AND_SYNC);
+ wizard_->Step(SyncSetupWizard::DONE);
+ test_window_->flow_->OnDialogClosed("");
+ EXPECT_FALSE(wizard_->IsVisible());
+ EXPECT_TRUE(service_->profile()->GetPrefs()->GetBoolean(
+ prefs::kSyncHasSetupCompleted));
+}
+
+TEST_F(SyncSetupWizardTest, DiscreteRun) {
+ DictionaryValue dialog_args;
+ // For a discrete run, we need to have ran through setup once.
+ wizard_->Step(SyncSetupWizard::GAIA_LOGIN);
+ wizard_->Step(SyncSetupWizard::GAIA_SUCCESS);
+ wizard_->Step(SyncSetupWizard::MERGE_AND_SYNC);
+ wizard_->Step(SyncSetupWizard::DONE);
+ test_window_->flow_->OnDialogClosed("");
+ EXPECT_TRUE(test_window_->TestAndResetWasShowHTMLDialogCalled());
+
+ wizard_->Step(SyncSetupWizard::GAIA_LOGIN);
+ EXPECT_EQ(SyncSetupWizard::GAIA_SUCCESS, test_window_->flow_->end_state_);
+
+ service_->set_auth_state(kTestUser, AUTH_ERROR_INVALID_GAIA_CREDENTIALS);
+ wizard_->Step(SyncSetupWizard::GAIA_LOGIN);
+ EXPECT_TRUE(wizard_->IsVisible());
+ SyncSetupFlow::GetArgsForGaiaLogin(service_, &dialog_args);
+ EXPECT_EQ(2, dialog_args.GetSize());
+ std::string actual_user;
+ dialog_args.GetString(L"user", &actual_user);
+ EXPECT_EQ(kTestUser, actual_user);
+ int error = -1;
+ dialog_args.GetInteger(L"error", &error);
+ EXPECT_EQ(static_cast<int>(AUTH_ERROR_INVALID_GAIA_CREDENTIALS), error);
+ service_->set_auth_state(kTestUser, AUTH_ERROR_NONE);
+
+ wizard_->Step(SyncSetupWizard::GAIA_SUCCESS);
+ EXPECT_TRUE(test_window_->TestAndResetWasShowHTMLDialogCalled());
+}
+
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/views/toolbar_view.cc b/chrome/browser/views/toolbar_view.cc
index 74f5a8b..68f5f35 100644
--- a/chrome/browser/views/toolbar_view.cc
+++ b/chrome/browser/views/toolbar_view.cc
@@ -40,7 +40,7 @@
#include "chrome/common/pref_names.h"
#include "chrome/common/pref_service.h"
#ifdef CHROME_PERSONALIZATION
-#include "chrome/personalization/personalization.h"
+#include "chrome/browser/sync/personalization.h"
#endif
#include "grit/chromium_strings.h"
#include "grit/generated_resources.h"