diff options
author | tim@chromium.org <tim@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-08-05 01:18:27 +0000 |
---|---|---|
committer | tim@chromium.org <tim@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-08-05 01:18:27 +0000 |
commit | 132c8565a63ad57d680b6b8d9beaa28786a46ea8 (patch) | |
tree | 428425605547842abd6a7ab851bd27132f11efde /chrome/browser/sync | |
parent | f902600776feea6570c876f2349bb4a8746ea95b (diff) | |
download | chromium_src-132c8565a63ad57d680b6b8d9beaa28786a46ea8.zip chromium_src-132c8565a63ad57d680b6b8d9beaa28786a46ea8.tar.gz chromium_src-132c8565a63ad57d680b6b8d9beaa28786a46ea8.tar.bz2 |
Add files to browser/sync and tweak includes.
Create browser/sync/glue and /engine.
Create sync watchlist and add a few folks.
No GYP change here so no build changes should occur.
chrome.gyp CL is coming shortly, as well as live_sync tests.
Review URL: http://codereview.chromium.org/160598
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@22454 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/sync')
25 files changed, 6903 insertions, 0 deletions
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 – +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 |