• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2013 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""An implementation of the server side of the Chromium sync protocol.
6
7The details of the protocol are described mostly by comments in the protocol
8buffer definition at chrome/browser/sync/protocol/sync.proto.
9"""
10
11import base64
12import cgi
13import copy
14import google.protobuf.text_format
15import hashlib
16import operator
17import pickle
18import random
19import string
20import sys
21import threading
22import time
23import urlparse
24import uuid
25
26import app_list_specifics_pb2
27import app_notification_specifics_pb2
28import app_setting_specifics_pb2
29import app_specifics_pb2
30import article_specifics_pb2
31import autofill_specifics_pb2
32import bookmark_specifics_pb2
33import client_commands_pb2
34import dictionary_specifics_pb2
35import get_updates_caller_info_pb2
36import extension_setting_specifics_pb2
37import extension_specifics_pb2
38import favicon_image_specifics_pb2
39import favicon_tracking_specifics_pb2
40import history_delete_directive_specifics_pb2
41import managed_user_setting_specifics_pb2
42import managed_user_specifics_pb2
43import managed_user_shared_setting_specifics_pb2
44import nigori_specifics_pb2
45import password_specifics_pb2
46import preference_specifics_pb2
47import priority_preference_specifics_pb2
48import search_engine_specifics_pb2
49import session_specifics_pb2
50import sync_pb2
51import sync_enums_pb2
52import synced_notification_app_info_specifics_pb2
53import synced_notification_data_pb2
54import synced_notification_render_pb2
55import synced_notification_specifics_pb2
56import theme_specifics_pb2
57import typed_url_specifics_pb2
58
59# An enumeration of the various kinds of data that can be synced.
60# Over the wire, this enumeration is not used: a sync object's type is
61# inferred by which EntitySpecifics field it has.  But in the context
62# of a program, it is useful to have an enumeration.
63ALL_TYPES = (
64    TOP_LEVEL,  # The type of the 'Google Chrome' folder.
65    APPS,
66    APP_LIST,
67    APP_NOTIFICATION,
68    APP_SETTINGS,
69    ARTICLE,
70    AUTOFILL,
71    AUTOFILL_PROFILE,
72    BOOKMARK,
73    DEVICE_INFO,
74    DICTIONARY,
75    EXPERIMENTS,
76    EXTENSIONS,
77    HISTORY_DELETE_DIRECTIVE,
78    MANAGED_USER_SETTING,
79    MANAGED_USER_SHARED_SETTING,
80    MANAGED_USER,
81    NIGORI,
82    PASSWORD,
83    PREFERENCE,
84    PRIORITY_PREFERENCE,
85    SEARCH_ENGINE,
86    SESSION,
87    SYNCED_NOTIFICATION,
88    SYNCED_NOTIFICATION_APP_INFO,
89    THEME,
90    TYPED_URL,
91    EXTENSION_SETTINGS,
92    FAVICON_IMAGES,
93    FAVICON_TRACKING) = range(30)
94
95# An enumeration on the frequency at which the server should send errors
96# to the client. This would be specified by the url that triggers the error.
97# Note: This enum should be kept in the same order as the enum in sync_test.h.
98SYNC_ERROR_FREQUENCY = (
99    ERROR_FREQUENCY_NONE,
100    ERROR_FREQUENCY_ALWAYS,
101    ERROR_FREQUENCY_TWO_THIRDS) = range(3)
102
103# Well-known server tag of the top level 'Google Chrome' folder.
104TOP_LEVEL_FOLDER_TAG = 'google_chrome'
105
106# Given a sync type from ALL_TYPES, find the FieldDescriptor corresponding
107# to that datatype.  Note that TOP_LEVEL has no such token.
108SYNC_TYPE_FIELDS = sync_pb2.EntitySpecifics.DESCRIPTOR.fields_by_name
109SYNC_TYPE_TO_DESCRIPTOR = {
110    APP_LIST: SYNC_TYPE_FIELDS['app_list'],
111    APP_NOTIFICATION: SYNC_TYPE_FIELDS['app_notification'],
112    APP_SETTINGS: SYNC_TYPE_FIELDS['app_setting'],
113    APPS: SYNC_TYPE_FIELDS['app'],
114    ARTICLE: SYNC_TYPE_FIELDS['article'],
115    AUTOFILL: SYNC_TYPE_FIELDS['autofill'],
116    AUTOFILL_PROFILE: SYNC_TYPE_FIELDS['autofill_profile'],
117    BOOKMARK: SYNC_TYPE_FIELDS['bookmark'],
118    DEVICE_INFO: SYNC_TYPE_FIELDS['device_info'],
119    DICTIONARY: SYNC_TYPE_FIELDS['dictionary'],
120    EXPERIMENTS: SYNC_TYPE_FIELDS['experiments'],
121    EXTENSION_SETTINGS: SYNC_TYPE_FIELDS['extension_setting'],
122    EXTENSIONS: SYNC_TYPE_FIELDS['extension'],
123    FAVICON_IMAGES: SYNC_TYPE_FIELDS['favicon_image'],
124    FAVICON_TRACKING: SYNC_TYPE_FIELDS['favicon_tracking'],
125    HISTORY_DELETE_DIRECTIVE: SYNC_TYPE_FIELDS['history_delete_directive'],
126    MANAGED_USER_SHARED_SETTING:
127        SYNC_TYPE_FIELDS['managed_user_shared_setting'],
128    MANAGED_USER_SETTING: SYNC_TYPE_FIELDS['managed_user_setting'],
129    MANAGED_USER: SYNC_TYPE_FIELDS['managed_user'],
130    NIGORI: SYNC_TYPE_FIELDS['nigori'],
131    PASSWORD: SYNC_TYPE_FIELDS['password'],
132    PREFERENCE: SYNC_TYPE_FIELDS['preference'],
133    PRIORITY_PREFERENCE: SYNC_TYPE_FIELDS['priority_preference'],
134    SEARCH_ENGINE: SYNC_TYPE_FIELDS['search_engine'],
135    SESSION: SYNC_TYPE_FIELDS['session'],
136    SYNCED_NOTIFICATION: SYNC_TYPE_FIELDS["synced_notification"],
137    SYNCED_NOTIFICATION_APP_INFO:
138        SYNC_TYPE_FIELDS["synced_notification_app_info"],
139    THEME: SYNC_TYPE_FIELDS['theme'],
140    TYPED_URL: SYNC_TYPE_FIELDS['typed_url'],
141    }
142
143# The parent ID used to indicate a top-level node.
144ROOT_ID = '0'
145
146# Unix time epoch +1 day in struct_time format. The tuple corresponds to
147# UTC Thursday Jan 2 1970, 00:00:00, non-dst.
148# We have to add one day after start of epoch, since in timezones with positive
149# UTC offset time.mktime throws an OverflowError,
150# rather then returning negative number.
151FIRST_DAY_UNIX_TIME_EPOCH = (1970, 1, 2, 0, 0, 0, 4, 2, 0)
152ONE_DAY_SECONDS = 60 * 60 * 24
153
154# The number of characters in the server-generated encryption key.
155KEYSTORE_KEY_LENGTH = 16
156
157# The hashed client tags for some experiment nodes.
158KEYSTORE_ENCRYPTION_EXPERIMENT_TAG = "pis8ZRzh98/MKLtVEio2mr42LQA="
159PRE_COMMIT_GU_AVOIDANCE_EXPERIMENT_TAG = "Z1xgeh3QUBa50vdEPd8C/4c7jfE="
160
161class Error(Exception):
162  """Error class for this module."""
163
164
165class ProtobufDataTypeFieldNotUnique(Error):
166  """An entry should not have more than one data type present."""
167
168
169class DataTypeIdNotRecognized(Error):
170  """The requested data type is not recognized."""
171
172
173class MigrationDoneError(Error):
174  """A server-side migration occurred; clients must re-sync some datatypes.
175
176  Attributes:
177    datatypes: a list of the datatypes (python enum) needing migration.
178  """
179
180  def __init__(self, datatypes):
181    self.datatypes = datatypes
182
183
184class StoreBirthdayError(Error):
185  """The client sent a birthday that doesn't correspond to this server."""
186
187
188class TransientError(Error):
189  """The client would be sent a transient error."""
190
191
192class SyncInducedError(Error):
193  """The client would be sent an error."""
194
195
196class InducedErrorFrequencyNotDefined(Error):
197  """The error frequency defined is not handled."""
198
199
200class ClientNotConnectedError(Error):
201  """The client is not connected to the server."""
202
203
204def GetEntryType(entry):
205  """Extract the sync type from a SyncEntry.
206
207  Args:
208    entry: A SyncEntity protobuf object whose type to determine.
209  Returns:
210    An enum value from ALL_TYPES if the entry's type can be determined, or None
211    if the type cannot be determined.
212  Raises:
213    ProtobufDataTypeFieldNotUnique: More than one type was indicated by
214    the entry.
215  """
216  if entry.server_defined_unique_tag == TOP_LEVEL_FOLDER_TAG:
217    return TOP_LEVEL
218  entry_types = GetEntryTypesFromSpecifics(entry.specifics)
219  if not entry_types:
220    return None
221
222  # If there is more than one, either there's a bug, or else the caller
223  # should use GetEntryTypes.
224  if len(entry_types) > 1:
225    raise ProtobufDataTypeFieldNotUnique
226  return entry_types[0]
227
228
229def GetEntryTypesFromSpecifics(specifics):
230  """Determine the sync types indicated by an EntitySpecifics's field(s).
231
232  If the specifics have more than one recognized data type field (as commonly
233  happens with the requested_types field of GetUpdatesMessage), all types
234  will be returned.  Callers must handle the possibility of the returned
235  value having more than one item.
236
237  Args:
238    specifics: A EntitySpecifics protobuf message whose extensions to
239      enumerate.
240  Returns:
241    A list of the sync types (values from ALL_TYPES) associated with each
242    recognized extension of the specifics message.
243  """
244  return [data_type for data_type, field_descriptor
245          in SYNC_TYPE_TO_DESCRIPTOR.iteritems()
246          if specifics.HasField(field_descriptor.name)]
247
248
249def SyncTypeToProtocolDataTypeId(data_type):
250  """Convert from a sync type (python enum) to the protocol's data type id."""
251  return SYNC_TYPE_TO_DESCRIPTOR[data_type].number
252
253
254def ProtocolDataTypeIdToSyncType(protocol_data_type_id):
255  """Convert from the protocol's data type id to a sync type (python enum)."""
256  for data_type, field_descriptor in SYNC_TYPE_TO_DESCRIPTOR.iteritems():
257    if field_descriptor.number == protocol_data_type_id:
258      return data_type
259  raise DataTypeIdNotRecognized
260
261
262def DataTypeStringToSyncTypeLoose(data_type_string):
263  """Converts a human-readable string to a sync type (python enum).
264
265  Capitalization and pluralization don't matter; this function is appropriate
266  for values that might have been typed by a human being; e.g., command-line
267  flags or query parameters.
268  """
269  if data_type_string.isdigit():
270    return ProtocolDataTypeIdToSyncType(int(data_type_string))
271  name = data_type_string.lower().rstrip('s')
272  for data_type, field_descriptor in SYNC_TYPE_TO_DESCRIPTOR.iteritems():
273    if field_descriptor.name.lower().rstrip('s') == name:
274      return data_type
275  raise DataTypeIdNotRecognized
276
277
278def MakeNewKeystoreKey():
279  """Returns a new random keystore key."""
280  return ''.join(random.choice(string.ascii_uppercase + string.digits)
281        for x in xrange(KEYSTORE_KEY_LENGTH))
282
283
284def SyncTypeToString(data_type):
285  """Formats a sync type enum (from ALL_TYPES) to a human-readable string."""
286  return SYNC_TYPE_TO_DESCRIPTOR[data_type].name
287
288
289def CallerInfoToString(caller_info_source):
290  """Formats a GetUpdatesSource enum value to a readable string."""
291  return get_updates_caller_info_pb2.GetUpdatesCallerInfo \
292      .DESCRIPTOR.enum_types_by_name['GetUpdatesSource'] \
293      .values_by_number[caller_info_source].name
294
295
296def ShortDatatypeListSummary(data_types):
297  """Formats compactly a list of sync types (python enums) for human eyes.
298
299  This function is intended for use by logging.  If the list of datatypes
300  contains almost all of the values, the return value will be expressed
301  in terms of the datatypes that aren't set.
302  """
303  included = set(data_types) - set([TOP_LEVEL])
304  if not included:
305    return 'nothing'
306  excluded = set(ALL_TYPES) - included - set([TOP_LEVEL])
307  if not excluded:
308    return 'everything'
309  simple_text = '+'.join(sorted([SyncTypeToString(x) for x in included]))
310  all_but_text = 'all except %s' % (
311      '+'.join(sorted([SyncTypeToString(x) for x in excluded])))
312  if len(included) < len(excluded) or len(simple_text) <= len(all_but_text):
313    return simple_text
314  else:
315    return all_but_text
316
317
318def GetDefaultEntitySpecifics(data_type):
319  """Get an EntitySpecifics having a sync type's default field value."""
320  specifics = sync_pb2.EntitySpecifics()
321  if data_type in SYNC_TYPE_TO_DESCRIPTOR:
322    descriptor = SYNC_TYPE_TO_DESCRIPTOR[data_type]
323    getattr(specifics, descriptor.name).SetInParent()
324  return specifics
325
326
327class PermanentItem(object):
328  """A specification of one server-created permanent item.
329
330  Attributes:
331    tag: A known-to-the-client value that uniquely identifies a server-created
332      permanent item.
333    name: The human-readable display name for this item.
334    parent_tag: The tag of the permanent item's parent.  If ROOT_ID, indicates
335      a top-level item.  Otherwise, this must be the tag value of some other
336      server-created permanent item.
337    sync_type: A value from ALL_TYPES, giving the datatype of this permanent
338      item.  This controls which types of client GetUpdates requests will
339      cause the permanent item to be created and returned.
340    create_by_default: Whether the permanent item is created at startup or not.
341      This value is set to True in the default case. Non-default permanent items
342      are those that are created only when a client explicitly tells the server
343      to do so.
344  """
345
346  def __init__(self, tag, name, parent_tag, sync_type, create_by_default=True):
347    self.tag = tag
348    self.name = name
349    self.parent_tag = parent_tag
350    self.sync_type = sync_type
351    self.create_by_default = create_by_default
352
353
354class MigrationHistory(object):
355  """A record of the migration events associated with an account.
356
357  Each migration event invalidates one or more datatypes on all clients
358  that had synced the datatype before the event.  Such clients will continue
359  to receive MigrationDone errors until they throw away their progress and
360  re-sync that datatype from the beginning.
361  """
362  def __init__(self):
363    self._migrations = {}
364    for datatype in ALL_TYPES:
365      self._migrations[datatype] = [1]
366    self._next_migration_version = 2
367
368  def GetLatestVersion(self, datatype):
369    return self._migrations[datatype][-1]
370
371  def CheckAllCurrent(self, versions_map):
372    """Raises an error if any the provided versions are out of date.
373
374    This function intentionally returns migrations in the order that they were
375    triggered.  Doing it this way allows the client to queue up two migrations
376    in a row, so the second one is received while responding to the first.
377
378    Arguments:
379      version_map: a map whose keys are datatypes and whose values are versions.
380
381    Raises:
382      MigrationDoneError: if a mismatch is found.
383    """
384    problems = {}
385    for datatype, client_migration in versions_map.iteritems():
386      for server_migration in self._migrations[datatype]:
387        if client_migration < server_migration:
388          problems.setdefault(server_migration, []).append(datatype)
389    if problems:
390      raise MigrationDoneError(problems[min(problems.keys())])
391
392  def Bump(self, datatypes):
393    """Add a record of a migration, to cause errors on future requests."""
394    for idx, datatype in enumerate(datatypes):
395      self._migrations[datatype].append(self._next_migration_version)
396    self._next_migration_version += 1
397
398
399class UpdateSieve(object):
400  """A filter to remove items the client has already seen."""
401  def __init__(self, request, migration_history=None):
402    self._original_request = request
403    self._state = {}
404    self._migration_history = migration_history or MigrationHistory()
405    self._migration_versions_to_check = {}
406    if request.from_progress_marker:
407      for marker in request.from_progress_marker:
408        data_type = ProtocolDataTypeIdToSyncType(marker.data_type_id)
409        if marker.HasField('timestamp_token_for_migration'):
410          timestamp = marker.timestamp_token_for_migration
411          if timestamp:
412            self._migration_versions_to_check[data_type] = 1
413        elif marker.token:
414          (timestamp, version) = pickle.loads(marker.token)
415          self._migration_versions_to_check[data_type] = version
416        elif marker.HasField('token'):
417          timestamp = 0
418        else:
419          raise ValueError('No timestamp information in progress marker.')
420        data_type = ProtocolDataTypeIdToSyncType(marker.data_type_id)
421        self._state[data_type] = timestamp
422    elif request.HasField('from_timestamp'):
423      for data_type in GetEntryTypesFromSpecifics(request.requested_types):
424        self._state[data_type] = request.from_timestamp
425        self._migration_versions_to_check[data_type] = 1
426    if self._state:
427      self._state[TOP_LEVEL] = min(self._state.itervalues())
428
429  def SummarizeRequest(self):
430    timestamps = {}
431    for data_type, timestamp in self._state.iteritems():
432      if data_type == TOP_LEVEL:
433        continue
434      timestamps.setdefault(timestamp, []).append(data_type)
435    return ', '.join('<%s>@%d' % (ShortDatatypeListSummary(types), stamp)
436                     for stamp, types in sorted(timestamps.iteritems()))
437
438  def CheckMigrationState(self):
439    self._migration_history.CheckAllCurrent(self._migration_versions_to_check)
440
441  def ClientWantsItem(self, item):
442    """Return true if the client hasn't already seen an item."""
443    return self._state.get(GetEntryType(item), sys.maxint) < item.version
444
445  def HasAnyTimestamp(self):
446    """Return true if at least one datatype was requested."""
447    return bool(self._state)
448
449  def GetMinTimestamp(self):
450    """Return true the smallest timestamp requested across all datatypes."""
451    return min(self._state.itervalues())
452
453  def GetFirstTimeTypes(self):
454    """Return a list of datatypes requesting updates from timestamp zero."""
455    return [datatype for datatype, timestamp in self._state.iteritems()
456            if timestamp == 0]
457
458  def GetCreateMobileBookmarks(self):
459    """Return true if the client has requested to create the 'Mobile Bookmarks'
460       folder.
461    """
462    return (self._original_request.HasField('create_mobile_bookmarks_folder')
463            and self._original_request.create_mobile_bookmarks_folder)
464
465  def SaveProgress(self, new_timestamp, get_updates_response):
466    """Write the new_timestamp or new_progress_marker fields to a response."""
467    if self._original_request.from_progress_marker:
468      for data_type, old_timestamp in self._state.iteritems():
469        if data_type == TOP_LEVEL:
470          continue
471        new_marker = sync_pb2.DataTypeProgressMarker()
472        new_marker.data_type_id = SyncTypeToProtocolDataTypeId(data_type)
473        final_stamp = max(old_timestamp, new_timestamp)
474        final_migration = self._migration_history.GetLatestVersion(data_type)
475        new_marker.token = pickle.dumps((final_stamp, final_migration))
476        get_updates_response.new_progress_marker.add().MergeFrom(new_marker)
477    elif self._original_request.HasField('from_timestamp'):
478      if self._original_request.from_timestamp < new_timestamp:
479        get_updates_response.new_timestamp = new_timestamp
480
481
482class SyncDataModel(object):
483  """Models the account state of one sync user."""
484  _BATCH_SIZE = 100
485
486  # Specify all the permanent items that a model might need.
487  _PERMANENT_ITEM_SPECS = [
488      PermanentItem('google_chrome_apps', name='Apps',
489                    parent_tag=ROOT_ID, sync_type=APPS),
490      PermanentItem('google_chrome_app_list', name='App List',
491                    parent_tag=ROOT_ID, sync_type=APP_LIST),
492      PermanentItem('google_chrome_app_notifications', name='App Notifications',
493                    parent_tag=ROOT_ID, sync_type=APP_NOTIFICATION),
494      PermanentItem('google_chrome_app_settings',
495                    name='App Settings',
496                    parent_tag=ROOT_ID, sync_type=APP_SETTINGS),
497      PermanentItem('google_chrome_bookmarks', name='Bookmarks',
498                    parent_tag=ROOT_ID, sync_type=BOOKMARK),
499      PermanentItem('bookmark_bar', name='Bookmark Bar',
500                    parent_tag='google_chrome_bookmarks', sync_type=BOOKMARK),
501      PermanentItem('other_bookmarks', name='Other Bookmarks',
502                    parent_tag='google_chrome_bookmarks', sync_type=BOOKMARK),
503      PermanentItem('synced_bookmarks', name='Synced Bookmarks',
504                    parent_tag='google_chrome_bookmarks', sync_type=BOOKMARK,
505                    create_by_default=False),
506      PermanentItem('google_chrome_autofill', name='Autofill',
507                    parent_tag=ROOT_ID, sync_type=AUTOFILL),
508      PermanentItem('google_chrome_autofill_profiles', name='Autofill Profiles',
509                    parent_tag=ROOT_ID, sync_type=AUTOFILL_PROFILE),
510      PermanentItem('google_chrome_device_info', name='Device Info',
511                    parent_tag=ROOT_ID, sync_type=DEVICE_INFO),
512      PermanentItem('google_chrome_experiments', name='Experiments',
513                    parent_tag=ROOT_ID, sync_type=EXPERIMENTS),
514      PermanentItem('google_chrome_extension_settings',
515                    name='Extension Settings',
516                    parent_tag=ROOT_ID, sync_type=EXTENSION_SETTINGS),
517      PermanentItem('google_chrome_extensions', name='Extensions',
518                    parent_tag=ROOT_ID, sync_type=EXTENSIONS),
519      PermanentItem('google_chrome_history_delete_directives',
520                    name='History Delete Directives',
521                    parent_tag=ROOT_ID,
522                    sync_type=HISTORY_DELETE_DIRECTIVE),
523      PermanentItem('google_chrome_favicon_images',
524                    name='Favicon Images',
525                    parent_tag=ROOT_ID,
526                    sync_type=FAVICON_IMAGES),
527      PermanentItem('google_chrome_favicon_tracking',
528                    name='Favicon Tracking',
529                    parent_tag=ROOT_ID,
530                    sync_type=FAVICON_TRACKING),
531      PermanentItem('google_chrome_managed_user_settings',
532                    name='Managed User Settings',
533                    parent_tag=ROOT_ID, sync_type=MANAGED_USER_SETTING),
534      PermanentItem('google_chrome_managed_users',
535                    name='Managed Users',
536                    parent_tag=ROOT_ID, sync_type=MANAGED_USER),
537      PermanentItem('google_chrome_managed_user_shared_settings',
538                    name='Managed User Shared Settings',
539                    parent_tag=ROOT_ID, sync_type=MANAGED_USER_SHARED_SETTING),
540      PermanentItem('google_chrome_nigori', name='Nigori',
541                    parent_tag=ROOT_ID, sync_type=NIGORI),
542      PermanentItem('google_chrome_passwords', name='Passwords',
543                    parent_tag=ROOT_ID, sync_type=PASSWORD),
544      PermanentItem('google_chrome_preferences', name='Preferences',
545                    parent_tag=ROOT_ID, sync_type=PREFERENCE),
546      PermanentItem('google_chrome_priority_preferences',
547                    name='Priority Preferences',
548                    parent_tag=ROOT_ID, sync_type=PRIORITY_PREFERENCE),
549      PermanentItem('google_chrome_synced_notifications',
550                    name='Synced Notifications',
551                    parent_tag=ROOT_ID, sync_type=SYNCED_NOTIFICATION),
552      PermanentItem('google_chrome_synced_notification_app_info',
553                    name='Synced Notification App Info',
554                    parent_tag=ROOT_ID, sync_type=SYNCED_NOTIFICATION_APP_INFO),
555      PermanentItem('google_chrome_search_engines', name='Search Engines',
556                    parent_tag=ROOT_ID, sync_type=SEARCH_ENGINE),
557      PermanentItem('google_chrome_sessions', name='Sessions',
558                    parent_tag=ROOT_ID, sync_type=SESSION),
559      PermanentItem('google_chrome_themes', name='Themes',
560                    parent_tag=ROOT_ID, sync_type=THEME),
561      PermanentItem('google_chrome_typed_urls', name='Typed URLs',
562                    parent_tag=ROOT_ID, sync_type=TYPED_URL),
563      PermanentItem('google_chrome_dictionary', name='Dictionary',
564                    parent_tag=ROOT_ID, sync_type=DICTIONARY),
565      PermanentItem('google_chrome_articles', name='Articles',
566                    parent_tag=ROOT_ID, sync_type=ARTICLE),
567      ]
568
569  def __init__(self):
570    # Monotonically increasing version number.  The next object change will
571    # take on this value + 1.
572    self._version = 0
573
574    # The definitive copy of this client's items: a map from ID string to a
575    # SyncEntity protocol buffer.
576    self._entries = {}
577
578    self.ResetStoreBirthday()
579    self.migration_history = MigrationHistory()
580    self.induced_error = sync_pb2.ClientToServerResponse.Error()
581    self.induced_error_frequency = 0
582    self.sync_count_before_errors = 0
583    self.acknowledge_managed_users = False
584    self._keys = [MakeNewKeystoreKey()]
585
586  def _SaveEntry(self, entry):
587    """Insert or update an entry in the change log, and give it a new version.
588
589    The ID fields of this entry are assumed to be valid server IDs.  This
590    entry will be updated with a new version number and sync_timestamp.
591
592    Args:
593      entry: The entry to be added or updated.
594    """
595    self._version += 1
596    # Maintain a global (rather than per-item) sequence number and use it
597    # both as the per-entry version as well as the update-progress timestamp.
598    # This simulates the behavior of the original server implementation.
599    entry.version = self._version
600    entry.sync_timestamp = self._version
601
602    # Preserve the originator info, which the client is not required to send
603    # when updating.
604    base_entry = self._entries.get(entry.id_string)
605    if base_entry:
606      entry.originator_cache_guid = base_entry.originator_cache_guid
607      entry.originator_client_item_id = base_entry.originator_client_item_id
608
609    self._entries[entry.id_string] = copy.deepcopy(entry)
610
611  def _ServerTagToId(self, tag):
612    """Determine the server ID from a server-unique tag.
613
614    The resulting value is guaranteed not to collide with the other ID
615    generation methods.
616
617    Args:
618      tag: The unique, known-to-the-client tag of a server-generated item.
619    Returns:
620      The string value of the computed server ID.
621    """
622    if not tag or tag == ROOT_ID:
623      return tag
624    spec = [x for x in self._PERMANENT_ITEM_SPECS if x.tag == tag][0]
625    return self._MakeCurrentId(spec.sync_type, '<server tag>%s' % tag)
626
627  def _TypeToTypeRootId(self, model_type):
628    """Returns the server ID for the type root node of the given type."""
629    tag = [x.tag for x in self._PERMANENT_ITEM_SPECS
630           if x.sync_type == model_type][0]
631    return self._ServerTagToId(tag)
632
633  def _ClientTagToId(self, datatype, tag):
634    """Determine the server ID from a client-unique tag.
635
636    The resulting value is guaranteed not to collide with the other ID
637    generation methods.
638
639    Args:
640      datatype: The sync type (python enum) of the identified object.
641      tag: The unique, opaque-to-the-server tag of a client-tagged item.
642    Returns:
643      The string value of the computed server ID.
644    """
645    return self._MakeCurrentId(datatype, '<client tag>%s' % tag)
646
647  def _ClientIdToId(self, datatype, client_guid, client_item_id):
648    """Compute a unique server ID from a client-local ID tag.
649
650    The resulting value is guaranteed not to collide with the other ID
651    generation methods.
652
653    Args:
654      datatype: The sync type (python enum) of the identified object.
655      client_guid: A globally unique ID that identifies the client which
656        created this item.
657      client_item_id: An ID that uniquely identifies this item on the client
658        which created it.
659    Returns:
660      The string value of the computed server ID.
661    """
662    # Using the client ID info is not required here (we could instead generate
663    # a random ID), but it's useful for debugging.
664    return self._MakeCurrentId(datatype,
665        '<server ID originally>%s/%s' % (client_guid, client_item_id))
666
667  def _MakeCurrentId(self, datatype, inner_id):
668    return '%d^%d^%s' % (datatype,
669                         self.migration_history.GetLatestVersion(datatype),
670                         inner_id)
671
672  def _ExtractIdInfo(self, id_string):
673    if not id_string or id_string == ROOT_ID:
674      return None
675    datatype_string, separator, remainder = id_string.partition('^')
676    migration_version_string, separator, inner_id = remainder.partition('^')
677    return (int(datatype_string), int(migration_version_string), inner_id)
678
679  def _WritePosition(self, entry, parent_id):
680    """Ensure the entry has an absolute, numeric position and parent_id.
681
682    Historically, clients would specify positions using the predecessor-based
683    references in the insert_after_item_id field; starting July 2011, this
684    was changed and Chrome now sends up the absolute position.  The server
685    must store a position_in_parent value and must not maintain
686    insert_after_item_id.
687    Starting in Jan 2013, the client will also send up a unique_position field
688    which should be saved and returned on subsequent GetUpdates.
689
690    Args:
691      entry: The entry for which to write a position.  Its ID field are
692        assumed to be server IDs.  This entry will have its parent_id_string,
693        position_in_parent and unique_position fields updated; its
694        insert_after_item_id field will be cleared.
695      parent_id: The ID of the entry intended as the new parent.
696    """
697
698    entry.parent_id_string = parent_id
699    if not entry.HasField('position_in_parent'):
700      entry.position_in_parent = 1337  # A debuggable, distinctive default.
701    entry.ClearField('insert_after_item_id')
702
703  def _ItemExists(self, id_string):
704    """Determine whether an item exists in the changelog."""
705    return id_string in self._entries
706
707  def _CreatePermanentItem(self, spec):
708    """Create one permanent item from its spec, if it doesn't exist.
709
710    The resulting item is added to the changelog.
711
712    Args:
713      spec: A PermanentItem object holding the properties of the item to create.
714    """
715    id_string = self._ServerTagToId(spec.tag)
716    if self._ItemExists(id_string):
717      return
718    print 'Creating permanent item: %s' % spec.name
719    entry = sync_pb2.SyncEntity()
720    entry.id_string = id_string
721    entry.non_unique_name = spec.name
722    entry.name = spec.name
723    entry.server_defined_unique_tag = spec.tag
724    entry.folder = True
725    entry.deleted = False
726    entry.specifics.CopyFrom(GetDefaultEntitySpecifics(spec.sync_type))
727    self._WritePosition(entry, self._ServerTagToId(spec.parent_tag))
728    self._SaveEntry(entry)
729
730  def _CreateDefaultPermanentItems(self, requested_types):
731    """Ensure creation of all default permanent items for a given set of types.
732
733    Args:
734      requested_types: A list of sync data types from ALL_TYPES.
735        All default permanent items of only these types will be created.
736    """
737    for spec in self._PERMANENT_ITEM_SPECS:
738      if spec.sync_type in requested_types and spec.create_by_default:
739        self._CreatePermanentItem(spec)
740
741  def ResetStoreBirthday(self):
742    """Resets the store birthday to a random value."""
743    # TODO(nick): uuid.uuid1() is better, but python 2.5 only.
744    self.store_birthday = '%0.30f' % random.random()
745
746  def StoreBirthday(self):
747    """Gets the store birthday."""
748    return self.store_birthday
749
750  def GetChanges(self, sieve):
751    """Get entries which have changed, oldest first.
752
753    The returned entries are limited to being _BATCH_SIZE many.  The entries
754    are returned in strict version order.
755
756    Args:
757      sieve: An update sieve to use to filter out updates the client
758        has already seen.
759    Returns:
760      A tuple of (version, entries, changes_remaining).  Version is a new
761      timestamp value, which should be used as the starting point for the
762      next query.  Entries is the batch of entries meeting the current
763      timestamp query.  Changes_remaining indicates the number of changes
764      left on the server after this batch.
765    """
766    if not sieve.HasAnyTimestamp():
767      return (0, [], 0)
768    min_timestamp = sieve.GetMinTimestamp()
769    first_time_types = sieve.GetFirstTimeTypes()
770    self._CreateDefaultPermanentItems(first_time_types)
771    # Mobile bookmark folder is not created by default, create it only when
772    # client requested it.
773    if (sieve.GetCreateMobileBookmarks() and
774        first_time_types.count(BOOKMARK) > 0):
775      self.TriggerCreateSyncedBookmarks()
776
777    self.TriggerAcknowledgeManagedUsers()
778
779    change_log = sorted(self._entries.values(),
780                        key=operator.attrgetter('version'))
781    new_changes = [x for x in change_log if x.version > min_timestamp]
782    # Pick batch_size new changes, and then filter them.  This matches
783    # the RPC behavior of the production sync server.
784    batch = new_changes[:self._BATCH_SIZE]
785    if not batch:
786      # Client is up to date.
787      return (min_timestamp, [], 0)
788
789    # Restrict batch to requested types.  Tombstones are untyped
790    # and will always get included.
791    filtered = [copy.deepcopy(item) for item in batch
792                if item.deleted or sieve.ClientWantsItem(item)]
793
794    # The new client timestamp is the timestamp of the last item in the
795    # batch, even if that item was filtered out.
796    return (batch[-1].version, filtered, len(new_changes) - len(batch))
797
798  def GetKeystoreKeys(self):
799    """Returns the encryption keys for this account."""
800    print "Returning encryption keys: %s" % self._keys
801    return self._keys
802
803  def _CopyOverImmutableFields(self, entry):
804    """Preserve immutable fields by copying pre-commit state.
805
806    Args:
807      entry: A sync entity from the client.
808    """
809    if entry.id_string in self._entries:
810      if self._entries[entry.id_string].HasField(
811          'server_defined_unique_tag'):
812        entry.server_defined_unique_tag = (
813            self._entries[entry.id_string].server_defined_unique_tag)
814
815  def _CheckVersionForCommit(self, entry):
816    """Perform an optimistic concurrency check on the version number.
817
818    Clients are only allowed to commit if they report having seen the most
819    recent version of an object.
820
821    Args:
822      entry: A sync entity from the client.  It is assumed that ID fields
823        have been converted to server IDs.
824    Returns:
825      A boolean value indicating whether the client's version matches the
826      newest server version for the given entry.
827    """
828    if entry.id_string in self._entries:
829      # Allow edits/deletes if the version matches, and any undeletion.
830      return (self._entries[entry.id_string].version == entry.version or
831              self._entries[entry.id_string].deleted)
832    else:
833      # Allow unknown ID only if the client thinks it's new too.
834      return entry.version == 0
835
836  def _CheckParentIdForCommit(self, entry):
837    """Check that the parent ID referenced in a SyncEntity actually exists.
838
839    Args:
840      entry: A sync entity from the client.  It is assumed that ID fields
841        have been converted to server IDs.
842    Returns:
843      A boolean value indicating whether the entity's parent ID is an object
844      that actually exists (and is not deleted) in the current account state.
845    """
846    if entry.parent_id_string == ROOT_ID:
847      # This is generally allowed.
848      return True
849    if (not entry.HasField('parent_id_string') and
850        entry.HasField('client_defined_unique_tag')):
851      return True  # Unique client tag items do not need to specify a parent.
852    if entry.parent_id_string not in self._entries:
853      print 'Warning: Client sent unknown ID.  Should never happen.'
854      return False
855    if entry.parent_id_string == entry.id_string:
856      print 'Warning: Client sent circular reference.  Should never happen.'
857      return False
858    if self._entries[entry.parent_id_string].deleted:
859      # This can happen in a race condition between two clients.
860      return False
861    if not self._entries[entry.parent_id_string].folder:
862      print 'Warning: Client sent non-folder parent.  Should never happen.'
863      return False
864    return True
865
866  def _RewriteIdsAsServerIds(self, entry, cache_guid, commit_session):
867    """Convert ID fields in a client sync entry to server IDs.
868
869    A commit batch sent by a client may contain new items for which the
870    server has not generated IDs yet.  And within a commit batch, later
871    items are allowed to refer to earlier items.  This method will
872    generate server IDs for new items, as well as rewrite references
873    to items whose server IDs were generated earlier in the batch.
874
875    Args:
876      entry: The client sync entry to modify.
877      cache_guid: The globally unique ID of the client that sent this
878        commit request.
879      commit_session: A dictionary mapping the original IDs to the new server
880        IDs, for any items committed earlier in the batch.
881    """
882    if entry.version == 0:
883      data_type = GetEntryType(entry)
884      if entry.HasField('client_defined_unique_tag'):
885        # When present, this should determine the item's ID.
886        new_id = self._ClientTagToId(data_type, entry.client_defined_unique_tag)
887      else:
888        new_id = self._ClientIdToId(data_type, cache_guid, entry.id_string)
889        entry.originator_cache_guid = cache_guid
890        entry.originator_client_item_id = entry.id_string
891      commit_session[entry.id_string] = new_id  # Remember the remapping.
892      entry.id_string = new_id
893    if entry.parent_id_string in commit_session:
894      entry.parent_id_string = commit_session[entry.parent_id_string]
895    if entry.insert_after_item_id in commit_session:
896      entry.insert_after_item_id = commit_session[entry.insert_after_item_id]
897
898  def ValidateCommitEntries(self, entries):
899    """Raise an exception if a commit batch contains any global errors.
900
901    Arguments:
902      entries: an iterable containing commit-form SyncEntity protocol buffers.
903
904    Raises:
905      MigrationDoneError: if any of the entries reference a recently-migrated
906        datatype.
907    """
908    server_ids_in_commit = set()
909    local_ids_in_commit = set()
910    for entry in entries:
911      if entry.version:
912        server_ids_in_commit.add(entry.id_string)
913      else:
914        local_ids_in_commit.add(entry.id_string)
915      if entry.HasField('parent_id_string'):
916        if entry.parent_id_string not in local_ids_in_commit:
917          server_ids_in_commit.add(entry.parent_id_string)
918
919    versions_present = {}
920    for server_id in server_ids_in_commit:
921      parsed = self._ExtractIdInfo(server_id)
922      if parsed:
923        datatype, version, _ = parsed
924        versions_present.setdefault(datatype, []).append(version)
925
926    self.migration_history.CheckAllCurrent(
927         dict((k, min(v)) for k, v in versions_present.iteritems()))
928
929  def CommitEntry(self, entry, cache_guid, commit_session):
930    """Attempt to commit one entry to the user's account.
931
932    Args:
933      entry: A SyncEntity protobuf representing desired object changes.
934      cache_guid: A string value uniquely identifying the client; this
935        is used for ID generation and will determine the originator_cache_guid
936        if the entry is new.
937      commit_session: A dictionary mapping client IDs to server IDs for any
938        objects committed earlier this session.  If the entry gets a new ID
939        during commit, the change will be recorded here.
940    Returns:
941      A SyncEntity reflecting the post-commit value of the entry, or None
942      if the entry was not committed due to an error.
943    """
944    entry = copy.deepcopy(entry)
945
946    # Generate server IDs for this entry, and write generated server IDs
947    # from earlier entries into the message's fields, as appropriate.  The
948    # ID generation state is stored in 'commit_session'.
949    self._RewriteIdsAsServerIds(entry, cache_guid, commit_session)
950
951    # Sets the parent ID field for a client-tagged item.  The client is allowed
952    # to not specify parents for these types of items.  The server can figure
953    # out on its own what the parent ID for this entry should be.
954    self._RewriteParentIdForUniqueClientEntry(entry)
955
956    # Perform the optimistic concurrency check on the entry's version number.
957    # Clients are not allowed to commit unless they indicate that they've seen
958    # the most recent version of an object.
959    if not self._CheckVersionForCommit(entry):
960      return None
961
962    # Check the validity of the parent ID; it must exist at this point.
963    # TODO(nick): Implement cycle detection and resolution.
964    if not self._CheckParentIdForCommit(entry):
965      return None
966
967    self._CopyOverImmutableFields(entry);
968
969    # At this point, the commit is definitely going to happen.
970
971    # Deletion works by storing a limited record for an entry, called a
972    # tombstone.  A sync server must track deleted IDs forever, since it does
973    # not keep track of client knowledge (there's no deletion ACK event).
974    if entry.deleted:
975      def MakeTombstone(id_string, datatype):
976        """Make a tombstone entry that will replace the entry being deleted.
977
978        Args:
979          id_string: Index of the SyncEntity to be deleted.
980        Returns:
981          A new SyncEntity reflecting the fact that the entry is deleted.
982        """
983        # Only the ID, version and deletion state are preserved on a tombstone.
984        tombstone = sync_pb2.SyncEntity()
985        tombstone.id_string = id_string
986        tombstone.deleted = True
987        tombstone.name = ''
988        tombstone.specifics.CopyFrom(GetDefaultEntitySpecifics(datatype))
989        return tombstone
990
991      def IsChild(child_id):
992        """Check if a SyncEntity is a child of entry, or any of its children.
993
994        Args:
995          child_id: Index of the SyncEntity that is a possible child of entry.
996        Returns:
997          True if it is a child; false otherwise.
998        """
999        if child_id not in self._entries:
1000          return False
1001        if self._entries[child_id].parent_id_string == entry.id_string:
1002          return True
1003        return IsChild(self._entries[child_id].parent_id_string)
1004
1005      # Identify any children entry might have.
1006      child_ids = [child.id_string for child in self._entries.itervalues()
1007                   if IsChild(child.id_string)]
1008
1009      # Mark all children that were identified as deleted.
1010      for child_id in child_ids:
1011        datatype = GetEntryType(self._entries[child_id])
1012        self._SaveEntry(MakeTombstone(child_id, datatype))
1013
1014      # Delete entry itself.
1015      datatype = GetEntryType(self._entries[entry.id_string])
1016      entry = MakeTombstone(entry.id_string, datatype)
1017    else:
1018      # Comments in sync.proto detail how the representation of positional
1019      # ordering works.
1020      #
1021      # We've almost fully deprecated the 'insert_after_item_id' field.
1022      # The 'position_in_parent' field is also deprecated, but as of Jan 2013
1023      # is still in common use.  The 'unique_position' field is the latest
1024      # and greatest in positioning technology.
1025      #
1026      # This server supports 'position_in_parent' and 'unique_position'.
1027      self._WritePosition(entry, entry.parent_id_string)
1028
1029    # Preserve the originator info, which the client is not required to send
1030    # when updating.
1031    base_entry = self._entries.get(entry.id_string)
1032    if base_entry and not entry.HasField('originator_cache_guid'):
1033      entry.originator_cache_guid = base_entry.originator_cache_guid
1034      entry.originator_client_item_id = base_entry.originator_client_item_id
1035
1036    # Store the current time since the Unix epoch in milliseconds.
1037    entry.mtime = (int((time.mktime(time.gmtime()) -
1038        (time.mktime(FIRST_DAY_UNIX_TIME_EPOCH) - ONE_DAY_SECONDS))*1000))
1039
1040    # Commit the change.  This also updates the version number.
1041    self._SaveEntry(entry)
1042    return entry
1043
1044  def _RewriteVersionInId(self, id_string):
1045    """Rewrites an ID so that its migration version becomes current."""
1046    parsed_id = self._ExtractIdInfo(id_string)
1047    if not parsed_id:
1048      return id_string
1049    datatype, old_migration_version, inner_id = parsed_id
1050    return self._MakeCurrentId(datatype, inner_id)
1051
1052  def _RewriteParentIdForUniqueClientEntry(self, entry):
1053    """Sets the entry's parent ID field to the appropriate value.
1054
1055    The client must always set enough of the specifics of the entries it sends
1056    up such that the server can identify its type.  (See crbug.com/373859)
1057
1058    The client is under no obligation to set the parent ID field.  The server
1059    can always infer what the appropriate parent for this model type should be.
1060    Having the client not send the parent ID is a step towards the removal of
1061    type root nodes.  (See crbug.com/373869)
1062
1063    This server implements these features by "faking" the existing of a parent
1064    ID early on in the commit processing.
1065
1066    This function has no effect on non-client-tagged items.
1067    """
1068    if not entry.HasField('client_defined_unique_tag'):
1069      return  # Skip this processing for non-client-tagged types.
1070    data_type = GetEntryType(entry)
1071    entry.parent_id_string = self._TypeToTypeRootId(data_type)
1072
1073  def TriggerMigration(self, datatypes):
1074    """Cause a migration to occur for a set of datatypes on this account.
1075
1076    Clients will see the MIGRATION_DONE error for these datatypes until they
1077    resync them.
1078    """
1079    versions_to_remap = self.migration_history.Bump(datatypes)
1080    all_entries = self._entries.values()
1081    self._entries.clear()
1082    for entry in all_entries:
1083      new_id = self._RewriteVersionInId(entry.id_string)
1084      entry.id_string = new_id
1085      if entry.HasField('parent_id_string'):
1086        entry.parent_id_string = self._RewriteVersionInId(
1087            entry.parent_id_string)
1088      self._entries[entry.id_string] = entry
1089
1090  def TriggerSyncTabFavicons(self):
1091    """Set the 'sync_tab_favicons' field to this account's nigori node.
1092
1093    If the field is not currently set, will write a new nigori node entry
1094    with the field set. Else does nothing.
1095    """
1096
1097    nigori_tag = "google_chrome_nigori"
1098    nigori_original = self._entries.get(self._ServerTagToId(nigori_tag))
1099    if (nigori_original.specifics.nigori.sync_tab_favicons):
1100      return
1101    nigori_new = copy.deepcopy(nigori_original)
1102    nigori_new.specifics.nigori.sync_tabs = True
1103    self._SaveEntry(nigori_new)
1104
1105  def TriggerCreateSyncedBookmarks(self):
1106    """Create the Synced Bookmarks folder under the Bookmarks permanent item.
1107
1108    Clients will then receive the Synced Bookmarks folder on future
1109    GetUpdates, and new bookmarks can be added within the Synced Bookmarks
1110    folder.
1111    """
1112
1113    synced_bookmarks_spec, = [spec for spec in self._PERMANENT_ITEM_SPECS
1114                              if spec.name == "Synced Bookmarks"]
1115    self._CreatePermanentItem(synced_bookmarks_spec)
1116
1117  def TriggerEnableKeystoreEncryption(self):
1118    """Create the keystore_encryption experiment entity and enable it.
1119
1120    A new entity within the EXPERIMENTS datatype is created with the unique
1121    client tag "keystore_encryption" if it doesn't already exist. The
1122    keystore_encryption message is then filled with |enabled| set to true.
1123    """
1124
1125    experiment_id = self._ServerTagToId("google_chrome_experiments")
1126    keystore_encryption_id = self._ClientTagToId(
1127        EXPERIMENTS,
1128        KEYSTORE_ENCRYPTION_EXPERIMENT_TAG)
1129    keystore_entry = self._entries.get(keystore_encryption_id)
1130    if keystore_entry is None:
1131      keystore_entry = sync_pb2.SyncEntity()
1132      keystore_entry.id_string = keystore_encryption_id
1133      keystore_entry.name = "Keystore Encryption"
1134      keystore_entry.client_defined_unique_tag = (
1135          KEYSTORE_ENCRYPTION_EXPERIMENT_TAG)
1136      keystore_entry.folder = False
1137      keystore_entry.deleted = False
1138      keystore_entry.specifics.CopyFrom(GetDefaultEntitySpecifics(EXPERIMENTS))
1139      self._WritePosition(keystore_entry, experiment_id)
1140
1141    keystore_entry.specifics.experiments.keystore_encryption.enabled = True
1142
1143    self._SaveEntry(keystore_entry)
1144
1145  def TriggerRotateKeystoreKeys(self):
1146    """Rotate the current set of keystore encryption keys.
1147
1148    |self._keys| will have a new random encryption key appended to it. We touch
1149    the nigori node so that each client will receive the new encryption keys
1150    only once.
1151    """
1152
1153    # Add a new encryption key.
1154    self._keys += [MakeNewKeystoreKey(), ]
1155
1156    # Increment the nigori node's timestamp, so clients will get the new keys
1157    # on their next GetUpdates (any time the nigori node is sent back, we also
1158    # send back the keystore keys).
1159    nigori_tag = "google_chrome_nigori"
1160    self._SaveEntry(self._entries.get(self._ServerTagToId(nigori_tag)))
1161
1162  def TriggerAcknowledgeManagedUsers(self):
1163    """Set the "acknowledged" flag for any managed user entities that don't have
1164       it set already.
1165    """
1166
1167    if not self.acknowledge_managed_users:
1168      return
1169
1170    managed_users = [copy.deepcopy(entry) for entry in self._entries.values()
1171                     if entry.specifics.HasField('managed_user')
1172                     and not entry.specifics.managed_user.acknowledged]
1173    for user in managed_users:
1174      user.specifics.managed_user.acknowledged = True
1175      self._SaveEntry(user)
1176
1177  def TriggerEnablePreCommitGetUpdateAvoidance(self):
1178    """Sets the experiment to enable pre-commit GetUpdate avoidance."""
1179    experiment_id = self._ServerTagToId("google_chrome_experiments")
1180    pre_commit_gu_avoidance_id = self._ClientTagToId(
1181        EXPERIMENTS,
1182        PRE_COMMIT_GU_AVOIDANCE_EXPERIMENT_TAG)
1183    entry = self._entries.get(pre_commit_gu_avoidance_id)
1184    if entry is None:
1185      entry = sync_pb2.SyncEntity()
1186      entry.id_string = pre_commit_gu_avoidance_id
1187      entry.name = "Pre-commit GU avoidance"
1188      entry.client_defined_unique_tag = PRE_COMMIT_GU_AVOIDANCE_EXPERIMENT_TAG
1189      entry.folder = False
1190      entry.deleted = False
1191      entry.specifics.CopyFrom(GetDefaultEntitySpecifics(EXPERIMENTS))
1192      self._WritePosition(entry, experiment_id)
1193    entry.specifics.experiments.pre_commit_update_avoidance.enabled = True
1194    self._SaveEntry(entry)
1195
1196  def SetInducedError(self, error, error_frequency,
1197                      sync_count_before_errors):
1198    self.induced_error = error
1199    self.induced_error_frequency = error_frequency
1200    self.sync_count_before_errors = sync_count_before_errors
1201
1202  def GetInducedError(self):
1203    return self.induced_error
1204
1205  def AddSyncedNotification(self, serialized_notification):
1206    """Adds a synced notification to the server data.
1207
1208    The notification will be delivered to the client on the next GetUpdates
1209    call.
1210
1211    Args:
1212      serialized_notification: A serialized CoalescedSyncedNotification.
1213
1214    Returns:
1215      The string representation of the added SyncEntity.
1216
1217    Raises:
1218      ClientNotConnectedError: if the client has not yet connected to this
1219      server
1220    """
1221    # A unique string used wherever a unique ID for this notification is
1222    # required.
1223    unique_notification_id = str(uuid.uuid4())
1224
1225    specifics = self._CreateSyncedNotificationEntitySpecifics(
1226        unique_notification_id, serialized_notification)
1227
1228    # Create the root SyncEntity representing a single notification.
1229    entity = sync_pb2.SyncEntity()
1230    entity.specifics.CopyFrom(specifics)
1231    entity.parent_id_string = self._ServerTagToId(
1232        'google_chrome_synced_notifications')
1233    entity.name = 'Synced notification added for testing'
1234    entity.version = self._GetNextVersionNumber()
1235
1236    entity.client_defined_unique_tag = self._CreateSyncedNotificationClientTag(
1237        specifics.synced_notification.coalesced_notification.key)
1238    entity.id_string = self._ClientTagToId(GetEntryType(entity),
1239                                           entity.client_defined_unique_tag)
1240
1241    self._entries[entity.id_string] = copy.deepcopy(entity)
1242
1243    return google.protobuf.text_format.MessageToString(entity)
1244
1245  def _GetNextVersionNumber(self):
1246    """Set the version to one more than the greatest version number seen."""
1247    entries = sorted(self._entries.values(), key=operator.attrgetter('version'))
1248    if len(entries) < 1:
1249      raise ClientNotConnectedError
1250    return entries[-1].version + 1
1251
1252  def _CreateSyncedNotificationEntitySpecifics(self, unique_id,
1253                                               serialized_notification):
1254    """Create the EntitySpecifics proto for a synced notification."""
1255    coalesced = synced_notification_data_pb2.CoalescedSyncedNotification()
1256    google.protobuf.text_format.Merge(serialized_notification, coalesced)
1257
1258    # Override the provided key so that we have a unique one.
1259    coalesced.key = unique_id
1260
1261    specifics = sync_pb2.EntitySpecifics()
1262    notification_specifics = \
1263        synced_notification_specifics_pb2.SyncedNotificationSpecifics()
1264    notification_specifics.coalesced_notification.CopyFrom(coalesced)
1265    specifics.synced_notification.CopyFrom(notification_specifics)
1266
1267    return specifics
1268
1269  def _CreateSyncedNotificationClientTag(self, key):
1270    """Create the client_defined_unique_tag value for a SyncedNotification.
1271
1272    Args:
1273      key: The entity used to create the client tag.
1274
1275    Returns:
1276      The string value of the to be used as the client_defined_unique_tag.
1277    """
1278    serialized_type = sync_pb2.EntitySpecifics()
1279    specifics = synced_notification_specifics_pb2.SyncedNotificationSpecifics()
1280    serialized_type.synced_notification.CopyFrom(specifics)
1281    hash_input = serialized_type.SerializeToString() + key
1282    return base64.b64encode(hashlib.sha1(hash_input).digest())
1283
1284  def AddSyncedNotificationAppInfo(self, app_info):
1285    """Adds an app info struct to the server data.
1286
1287    The notification will be delivered to the client on the next GetUpdates
1288    call.
1289
1290    Args:
1291      app_info: A serialized AppInfo.
1292
1293    Returns:
1294      The string representation of the added SyncEntity.
1295
1296    Raises:
1297      ClientNotConnectedError: if the client has not yet connected to this
1298      server
1299    """
1300    specifics = self._CreateSyncedNotificationAppInfoEntitySpecifics(app_info)
1301
1302    # Create the root SyncEntity representing a single app info protobuf.
1303    entity = sync_pb2.SyncEntity()
1304    entity.specifics.CopyFrom(specifics)
1305    entity.parent_id_string = self._ServerTagToId(
1306        'google_chrome_synced_notification_app_info')
1307    entity.name = 'App info added for testing'
1308    entity.version = self._GetNextVersionNumber()
1309
1310    # App Infos do not have a strong id, it only needs to be unique.
1311    entity.client_defined_unique_tag = "foo"
1312    entity.id_string = "foo"
1313
1314    self._entries[entity.id_string] = copy.deepcopy(entity)
1315
1316    print "entity before exit is ", entity
1317
1318    return google.protobuf.text_format.MessageToString(entity)
1319
1320  def _CreateSyncedNotificationAppInfoEntitySpecifics(
1321    self, synced_notification_app_info):
1322    """Create the EntitySpecifics proto for a synced notification app info."""
1323    # Create a single, empty app_info object
1324    app_info = \
1325      synced_notification_app_info_specifics_pb2.SyncedNotificationAppInfo()
1326    # Fill the app_info object from the text format protobuf.
1327    google.protobuf.text_format.Merge(synced_notification_app_info, app_info)
1328
1329    # Create a new specifics object with a contained app_info
1330    specifics = sync_pb2.EntitySpecifics()
1331    app_info_specifics = \
1332        synced_notification_app_info_specifics_pb2.\
1333        SyncedNotificationAppInfoSpecifics()
1334
1335    # Copy the app info from the text format protobuf
1336    contained_app_info = app_info_specifics.synced_notification_app_info.add()
1337    contained_app_info.CopyFrom(app_info)
1338
1339    # And put the new app_info_specifics into the specifics before returning.
1340    specifics.synced_notification_app_info.CopyFrom(app_info_specifics)
1341
1342    return specifics
1343
1344class TestServer(object):
1345  """An object to handle requests for one (and only one) Chrome Sync account.
1346
1347  TestServer consumes the sync command messages that are the outermost
1348  layers of the protocol, performs the corresponding actions on its
1349  SyncDataModel, and constructs an appropriate response message.
1350  """
1351
1352  def __init__(self):
1353    # The implementation supports exactly one account; its state is here.
1354    self.account = SyncDataModel()
1355    self.account_lock = threading.Lock()
1356    # Clients that have talked to us: a map from the full client ID
1357    # to its nickname.
1358    self.clients = {}
1359    self.client_name_generator = ('+' * times + chr(c)
1360        for times in xrange(0, sys.maxint) for c in xrange(ord('A'), ord('Z')))
1361    self.transient_error = False
1362    self.sync_count = 0
1363    # Gaia OAuth2 Token fields and their default values.
1364    self.response_code = 200
1365    self.request_token = 'rt1'
1366    self.access_token = 'at1'
1367    self.expires_in = 3600
1368    self.token_type = 'Bearer'
1369    # The ClientCommand to send back on each ServerToClientResponse. If set to
1370    # None, no ClientCommand should be sent.
1371    self._client_command = None
1372
1373
1374  def GetShortClientName(self, query):
1375    parsed = cgi.parse_qs(query[query.find('?')+1:])
1376    client_id = parsed.get('client_id')
1377    if not client_id:
1378      return '?'
1379    client_id = client_id[0]
1380    if client_id not in self.clients:
1381      self.clients[client_id] = self.client_name_generator.next()
1382    return self.clients[client_id]
1383
1384  def CheckStoreBirthday(self, request):
1385    """Raises StoreBirthdayError if the request's birthday is a mismatch."""
1386    if not request.HasField('store_birthday'):
1387      return
1388    if self.account.StoreBirthday() != request.store_birthday:
1389      raise StoreBirthdayError
1390
1391  def CheckTransientError(self):
1392    """Raises TransientError if transient_error variable is set."""
1393    if self.transient_error:
1394      raise TransientError
1395
1396  def CheckSendError(self):
1397     """Raises SyncInducedError if needed."""
1398     if (self.account.induced_error.error_type !=
1399         sync_enums_pb2.SyncEnums.UNKNOWN):
1400       # Always means return the given error for all requests.
1401       if self.account.induced_error_frequency == ERROR_FREQUENCY_ALWAYS:
1402         raise SyncInducedError
1403       # This means the FIRST 2 requests of every 3 requests
1404       # return an error. Don't switch the order of failures. There are
1405       # test cases that rely on the first 2 being the failure rather than
1406       # the last 2.
1407       elif (self.account.induced_error_frequency ==
1408             ERROR_FREQUENCY_TWO_THIRDS):
1409         if (((self.sync_count -
1410               self.account.sync_count_before_errors) % 3) != 0):
1411           raise SyncInducedError
1412       else:
1413         raise InducedErrorFrequencyNotDefined
1414
1415  def HandleMigrate(self, path):
1416    query = urlparse.urlparse(path)[4]
1417    code = 200
1418    self.account_lock.acquire()
1419    try:
1420      datatypes = [DataTypeStringToSyncTypeLoose(x)
1421                   for x in urlparse.parse_qs(query).get('type',[])]
1422      if datatypes:
1423        self.account.TriggerMigration(datatypes)
1424        response = 'Migrated datatypes %s' % (
1425            ' and '.join(SyncTypeToString(x).upper() for x in datatypes))
1426      else:
1427        response = 'Please specify one or more <i>type=name</i> parameters'
1428        code = 400
1429    except DataTypeIdNotRecognized, error:
1430      response = 'Could not interpret datatype name'
1431      code = 400
1432    finally:
1433      self.account_lock.release()
1434    return (code, '<html><title>Migration: %d</title><H1>%d %s</H1></html>' %
1435                (code, code, response))
1436
1437  def HandleSetInducedError(self, path):
1438     query = urlparse.urlparse(path)[4]
1439     self.account_lock.acquire()
1440     code = 200
1441     response = 'Success'
1442     error = sync_pb2.ClientToServerResponse.Error()
1443     try:
1444       error_type = urlparse.parse_qs(query)['error']
1445       action = urlparse.parse_qs(query)['action']
1446       error.error_type = int(error_type[0])
1447       error.action = int(action[0])
1448       try:
1449         error.url = (urlparse.parse_qs(query)['url'])[0]
1450       except KeyError:
1451         error.url = ''
1452       try:
1453         error.error_description =(
1454         (urlparse.parse_qs(query)['error_description'])[0])
1455       except KeyError:
1456         error.error_description = ''
1457       try:
1458         error_frequency = int((urlparse.parse_qs(query)['frequency'])[0])
1459       except KeyError:
1460         error_frequency = ERROR_FREQUENCY_ALWAYS
1461       self.account.SetInducedError(error, error_frequency, self.sync_count)
1462       response = ('Error = %d, action = %d, url = %s, description = %s' %
1463                   (error.error_type, error.action,
1464                    error.url,
1465                    error.error_description))
1466     except error:
1467       response = 'Could not parse url'
1468       code = 400
1469     finally:
1470       self.account_lock.release()
1471     return (code, '<html><title>SetError: %d</title><H1>%d %s</H1></html>' %
1472                (code, code, response))
1473
1474  def HandleCreateBirthdayError(self):
1475    self.account.ResetStoreBirthday()
1476    return (
1477        200,
1478        '<html><title>Birthday error</title><H1>Birthday error</H1></html>')
1479
1480  def HandleSetTransientError(self):
1481    self.transient_error = True
1482    return (
1483        200,
1484        '<html><title>Transient error</title><H1>Transient error</H1></html>')
1485
1486  def HandleSetSyncTabFavicons(self):
1487    """Set 'sync_tab_favicons' field of the nigori node for this account."""
1488    self.account.TriggerSyncTabFavicons()
1489    return (
1490        200,
1491        '<html><title>Tab Favicons</title><H1>Tab Favicons</H1></html>')
1492
1493  def HandleCreateSyncedBookmarks(self):
1494    """Create the Synced Bookmarks folder under Bookmarks."""
1495    self.account.TriggerCreateSyncedBookmarks()
1496    return (
1497        200,
1498        '<html><title>Synced Bookmarks</title><H1>Synced Bookmarks</H1></html>')
1499
1500  def HandleEnableKeystoreEncryption(self):
1501    """Enables the keystore encryption experiment."""
1502    self.account.TriggerEnableKeystoreEncryption()
1503    return (
1504        200,
1505        '<html><title>Enable Keystore Encryption</title>'
1506            '<H1>Enable Keystore Encryption</H1></html>')
1507
1508  def HandleRotateKeystoreKeys(self):
1509    """Rotate the keystore encryption keys."""
1510    self.account.TriggerRotateKeystoreKeys()
1511    return (
1512        200,
1513        '<html><title>Rotate Keystore Keys</title>'
1514            '<H1>Rotate Keystore Keys</H1></html>')
1515
1516  def HandleEnableManagedUserAcknowledgement(self):
1517    """Enable acknowledging newly created managed users."""
1518    self.account.acknowledge_managed_users = True
1519    return (
1520        200,
1521        '<html><title>Enable Managed User Acknowledgement</title>'
1522            '<h1>Enable Managed User Acknowledgement</h1></html>')
1523
1524  def HandleEnablePreCommitGetUpdateAvoidance(self):
1525    """Enables the pre-commit GU avoidance experiment."""
1526    self.account.TriggerEnablePreCommitGetUpdateAvoidance()
1527    return (
1528        200,
1529        '<html><title>Enable pre-commit GU avoidance</title>'
1530            '<H1>Enable pre-commit GU avoidance</H1></html>')
1531
1532  def HandleCommand(self, query, raw_request):
1533    """Decode and handle a sync command from a raw input of bytes.
1534
1535    This is the main entry point for this class.  It is safe to call this
1536    method from multiple threads.
1537
1538    Args:
1539      raw_request: An iterable byte sequence to be interpreted as a sync
1540        protocol command.
1541    Returns:
1542      A tuple (response_code, raw_response); the first value is an HTTP
1543      result code, while the second value is a string of bytes which is the
1544      serialized reply to the command.
1545    """
1546    self.account_lock.acquire()
1547    self.sync_count += 1
1548    def print_context(direction):
1549      print '[Client %s %s %s.py]' % (self.GetShortClientName(query), direction,
1550                                      __name__),
1551
1552    try:
1553      request = sync_pb2.ClientToServerMessage()
1554      request.MergeFromString(raw_request)
1555      contents = request.message_contents
1556
1557      response = sync_pb2.ClientToServerResponse()
1558      response.error_code = sync_enums_pb2.SyncEnums.SUCCESS
1559
1560      if self._client_command:
1561        response.client_command.CopyFrom(self._client_command)
1562
1563      self.CheckStoreBirthday(request)
1564      response.store_birthday = self.account.store_birthday
1565      self.CheckTransientError()
1566      self.CheckSendError()
1567
1568      print_context('->')
1569
1570      if contents == sync_pb2.ClientToServerMessage.AUTHENTICATE:
1571        print 'Authenticate'
1572        # We accept any authentication token, and support only one account.
1573        # TODO(nick): Mock out the GAIA authentication as well; hook up here.
1574        response.authenticate.user.email = 'syncjuser@chromium'
1575        response.authenticate.user.display_name = 'Sync J User'
1576      elif contents == sync_pb2.ClientToServerMessage.COMMIT:
1577        print 'Commit %d item(s)' % len(request.commit.entries)
1578        self.HandleCommit(request.commit, response.commit)
1579      elif contents == sync_pb2.ClientToServerMessage.GET_UPDATES:
1580        print 'GetUpdates',
1581        self.HandleGetUpdates(request.get_updates, response.get_updates)
1582        print_context('<-')
1583        print '%d update(s)' % len(response.get_updates.entries)
1584      else:
1585        print 'Unrecognizable sync request!'
1586        return (400, None)  # Bad request.
1587      return (200, response.SerializeToString())
1588    except MigrationDoneError, error:
1589      print_context('<-')
1590      print 'MIGRATION_DONE: <%s>' % (ShortDatatypeListSummary(error.datatypes))
1591      response = sync_pb2.ClientToServerResponse()
1592      response.store_birthday = self.account.store_birthday
1593      response.error_code = sync_enums_pb2.SyncEnums.MIGRATION_DONE
1594      response.migrated_data_type_id[:] = [
1595          SyncTypeToProtocolDataTypeId(x) for x in error.datatypes]
1596      return (200, response.SerializeToString())
1597    except StoreBirthdayError, error:
1598      print_context('<-')
1599      print 'NOT_MY_BIRTHDAY'
1600      response = sync_pb2.ClientToServerResponse()
1601      response.store_birthday = self.account.store_birthday
1602      response.error_code = sync_enums_pb2.SyncEnums.NOT_MY_BIRTHDAY
1603      return (200, response.SerializeToString())
1604    except TransientError, error:
1605      ### This is deprecated now. Would be removed once test cases are removed.
1606      print_context('<-')
1607      print 'TRANSIENT_ERROR'
1608      response.store_birthday = self.account.store_birthday
1609      response.error_code = sync_enums_pb2.SyncEnums.TRANSIENT_ERROR
1610      return (200, response.SerializeToString())
1611    except SyncInducedError, error:
1612      print_context('<-')
1613      print 'INDUCED_ERROR'
1614      response.store_birthday = self.account.store_birthday
1615      error = self.account.GetInducedError()
1616      response.error.error_type = error.error_type
1617      response.error.url = error.url
1618      response.error.error_description = error.error_description
1619      response.error.action = error.action
1620      return (200, response.SerializeToString())
1621    finally:
1622      self.account_lock.release()
1623
1624  def HandleCommit(self, commit_message, commit_response):
1625    """Respond to a Commit request by updating the user's account state.
1626
1627    Commit attempts stop after the first error, returning a CONFLICT result
1628    for any unattempted entries.
1629
1630    Args:
1631      commit_message: A sync_pb.CommitMessage protobuf holding the content
1632        of the client's request.
1633      commit_response: A sync_pb.CommitResponse protobuf into which a reply
1634        to the client request will be written.
1635    """
1636    commit_response.SetInParent()
1637    batch_failure = False
1638    session = {}  # Tracks ID renaming during the commit operation.
1639    guid = commit_message.cache_guid
1640
1641    self.account.ValidateCommitEntries(commit_message.entries)
1642
1643    for entry in commit_message.entries:
1644      server_entry = None
1645      if not batch_failure:
1646        # Try to commit the change to the account.
1647        server_entry = self.account.CommitEntry(entry, guid, session)
1648
1649      # An entryresponse is returned in both success and failure cases.
1650      reply = commit_response.entryresponse.add()
1651      if not server_entry:
1652        reply.response_type = sync_pb2.CommitResponse.CONFLICT
1653        reply.error_message = 'Conflict.'
1654        batch_failure = True  # One failure halts the batch.
1655      else:
1656        reply.response_type = sync_pb2.CommitResponse.SUCCESS
1657        # These are the properties that the server is allowed to override
1658        # during commit; the client wants to know their values at the end
1659        # of the operation.
1660        reply.id_string = server_entry.id_string
1661        if not server_entry.deleted:
1662          # Note: the production server doesn't actually send the
1663          # parent_id_string on commit responses, so we don't either.
1664          reply.position_in_parent = server_entry.position_in_parent
1665          reply.version = server_entry.version
1666          reply.name = server_entry.name
1667          reply.non_unique_name = server_entry.non_unique_name
1668        else:
1669          reply.version = entry.version + 1
1670
1671  def HandleGetUpdates(self, update_request, update_response):
1672    """Respond to a GetUpdates request by querying the user's account.
1673
1674    Args:
1675      update_request: A sync_pb.GetUpdatesMessage protobuf holding the content
1676        of the client's request.
1677      update_response: A sync_pb.GetUpdatesResponse protobuf into which a reply
1678        to the client request will be written.
1679    """
1680    update_response.SetInParent()
1681    update_sieve = UpdateSieve(update_request, self.account.migration_history)
1682
1683    print CallerInfoToString(update_request.caller_info.source),
1684    print update_sieve.SummarizeRequest()
1685
1686    update_sieve.CheckMigrationState()
1687
1688    new_timestamp, entries, remaining = self.account.GetChanges(update_sieve)
1689
1690    update_response.changes_remaining = remaining
1691    sending_nigori_node = False
1692    for entry in entries:
1693      if entry.name == 'Nigori':
1694        sending_nigori_node = True
1695      reply = update_response.entries.add()
1696      reply.CopyFrom(entry)
1697    update_sieve.SaveProgress(new_timestamp, update_response)
1698
1699    if update_request.need_encryption_key or sending_nigori_node:
1700      update_response.encryption_keys.extend(self.account.GetKeystoreKeys())
1701
1702  def HandleGetOauth2Token(self):
1703    return (int(self.response_code),
1704            '{\n'
1705            '  \"refresh_token\": \"' + self.request_token + '\",\n'
1706            '  \"access_token\": \"' + self.access_token + '\",\n'
1707            '  \"expires_in\": ' + str(self.expires_in) + ',\n'
1708            '  \"token_type\": \"' + self.token_type +'\"\n'
1709            '}')
1710
1711  def HandleSetOauth2Token(self, response_code, request_token, access_token,
1712                           expires_in, token_type):
1713    if response_code != 0:
1714      self.response_code = response_code
1715    if request_token != '':
1716      self.request_token = request_token
1717    if access_token != '':
1718      self.access_token = access_token
1719    if expires_in != 0:
1720      self.expires_in = expires_in
1721    if token_type != '':
1722      self.token_type = token_type
1723
1724    return (200,
1725            '<html><title>Set OAuth2 Token</title>'
1726            '<H1>This server will now return the OAuth2 Token:</H1>'
1727            '<p>response_code: ' + str(self.response_code) + '</p>'
1728            '<p>request_token: ' + self.request_token + '</p>'
1729            '<p>access_token: ' + self.access_token + '</p>'
1730            '<p>expires_in: ' + str(self.expires_in) + '</p>'
1731            '<p>token_type: ' + self.token_type + '</p>'
1732            '</html>')
1733
1734  def CustomizeClientCommand(self, sessions_commit_delay_seconds):
1735    """Customizes the value of the ClientCommand of ServerToClientResponse.
1736
1737    Currently, this only allows for changing the sessions_commit_delay_seconds
1738    field. This is useful for testing in conjunction with
1739    AddSyncedNotification so that synced notifications are seen immediately
1740    after triggering them with an HTTP call to the test server.
1741
1742    Args:
1743      sessions_commit_delay_seconds: The desired sync delay time for sessions.
1744    """
1745    if not self._client_command:
1746      self._client_command = client_commands_pb2.ClientCommand()
1747
1748    self._client_command.sessions_commit_delay_seconds = \
1749        sessions_commit_delay_seconds
1750    return self._client_command
1751