summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@qt.io>2022-05-11 15:53:45 +0200
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2022-05-11 15:53:45 +0200
commitce0174048ad7d3b56546ba93683241e9bc42f1f4 (patch)
treee1e74bf3d284ac370ad26560c303a98814072743
parent72d76568fe90b072c0199391b5b5b47c3052e79f (diff)
parent9729c4479fe23554eae6e6dd1f30ff488f470c84 (diff)
downloadqtwebengine-chromium-ce0174048ad7d3b56546ba93683241e9bc42f1f4.tar.gz
Merge remote-tracking branch 'origin/upstream-master' into 98-based
Change-Id: Id028ba69e041f2a69969a6a4b57b3af3fad8b1b6
-rw-r--r--chromium/chrome/browser/gcm/COMMON_METADATA4
-rw-r--r--chromium/chrome/browser/gcm/DIR_METADATA1
-rw-r--r--chromium/chrome/browser/gcm/OWNERS4
-rw-r--r--chromium/chrome/browser/gcm/gcm_product_util.cc57
-rw-r--r--chromium/chrome/browser/gcm/gcm_product_util.h30
-rw-r--r--chromium/chrome/browser/gcm/gcm_profile_service_factory.cc174
-rw-r--r--chromium/chrome/browser/gcm/gcm_profile_service_factory.h57
-rw-r--r--chromium/chrome/browser/gcm/gcm_profile_service_unittest.cc269
-rw-r--r--chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.cc60
-rw-r--r--chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h44
-rw-r--r--chromium/chrome/browser/profiles/incognito_helpers.cc28
-rw-r--r--chromium/chrome/browser/profiles/incognito_helpers.h29
-rw-r--r--chromium/chrome/browser/push_messaging/DIR_METADATA1
-rw-r--r--chromium/chrome/browser/push_messaging/OWNERS1
-rw-r--r--chromium/chrome/browser/push_messaging/budget.proto30
-rw-r--r--chromium/chrome/browser/push_messaging/budget_database.cc400
-rw-r--r--chromium/chrome/browser/push_messaging/budget_database.h182
-rw-r--r--chromium/chrome/browser/push_messaging/budget_database_unittest.cc351
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_app_identifier.cc322
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_app_identifier.h152
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_app_identifier_unittest.cc310
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_browsertest.cc3227
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_constants.cc11
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_constants.h25
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_features.cc15
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_features.h21
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_notification_manager.cc353
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_notification_manager.h122
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_notification_manager_unittest.cc87
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_refresher.cc121
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_refresher.h99
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_refresher_unittest.cc84
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_service_factory.cc82
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_service_factory.h40
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_service_impl.cc1684
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_service_impl.h475
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_service_unittest.cc480
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_utils.cc44
-rw-r--r--chromium/chrome/browser/push_messaging/push_messaging_utils.h34
-rw-r--r--chromium/chrome/browser/signin/DEPS3
-rw-r--r--chromium/chrome/browser/signin/DIR_METADATA1
-rw-r--r--chromium/chrome/browser/signin/OWNERS5
-rw-r--r--chromium/chrome/browser/signin/about_signin_internals_factory.cc58
-rw-r--r--chromium/chrome/browser/signin/about_signin_internals_factory.h40
-rw-r--r--chromium/chrome/browser/signin/account_consistency_mode_manager.cc208
-rw-r--r--chromium/chrome/browser/signin/account_consistency_mode_manager.h97
-rw-r--r--chromium/chrome/browser/signin/account_consistency_mode_manager_factory.cc53
-rw-r--r--chromium/chrome/browser/signin/account_consistency_mode_manager_factory.h35
-rw-r--r--chromium/chrome/browser/signin/account_consistency_mode_manager_unittest.cc259
-rw-r--r--chromium/chrome/browser/signin/account_id_from_account_info.cc24
-rw-r--r--chromium/chrome/browser/signin/account_id_from_account_info.h18
-rw-r--r--chromium/chrome/browser/signin/account_id_from_account_info_unittest.cc20
-rw-r--r--chromium/chrome/browser/signin/account_investigator_factory.cc57
-rw-r--r--chromium/chrome/browser/signin/account_investigator_factory.h44
-rw-r--r--chromium/chrome/browser/signin/account_reconcilor_factory.cc212
-rw-r--r--chromium/chrome/browser/signin/account_reconcilor_factory.h57
-rw-r--r--chromium/chrome/browser/signin/chrome_device_id_helper.cc87
-rw-r--r--chromium/chrome/browser/signin/chrome_device_id_helper.h36
-rw-r--r--chromium/chrome/browser/signin/chrome_device_id_helper_unittest.cc40
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_client.cc369
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_client.h115
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_client_factory.cc34
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_client_factory.h37
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_client_test_util.cc21
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_client_test_util.h26
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_client_unittest.cc438
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_helper.cc712
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_helper.h129
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_helper_unittest.cc182
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.cc551
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h100
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory_unittest.cc359
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.cc141
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h58
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate_unittest.cc61
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.cc189
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.h72
-rw-r--r--chromium/chrome/browser/signin/chrome_signin_url_loader_throttle_unittest.cc281
-rw-r--r--chromium/chrome/browser/signin/chromeos_mirror_account_consistency_browsertest.cc174
-rw-r--r--chromium/chrome/browser/signin/cookie_reminter_factory.cc37
-rw-r--r--chromium/chrome/browser/signin/cookie_reminter_factory.h30
-rw-r--r--chromium/chrome/browser/signin/dice_browsertest.cc1236
-rw-r--r--chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.cc179
-rw-r--r--chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.h102
-rw-r--r--chromium/chrome/browser/signin/dice_response_handler.cc426
-rw-r--r--chromium/chrome/browser/signin/dice_response_handler.h187
-rw-r--r--chromium/chrome/browser/signin/dice_response_handler_unittest.cc820
-rw-r--r--chromium/chrome/browser/signin/dice_signed_in_profile_creator.cc211
-rw-r--r--chromium/chrome/browser/signin/dice_signed_in_profile_creator.h70
-rw-r--r--chromium/chrome/browser/signin/dice_signed_in_profile_creator_unittest.cc285
-rw-r--r--chromium/chrome/browser/signin/dice_tab_helper.cc117
-rw-r--r--chromium/chrome/browser/signin/dice_tab_helper.h97
-rw-r--r--chromium/chrome/browser/signin/dice_tab_helper_unittest.cc253
-rw-r--r--chromium/chrome/browser/signin/dice_web_signin_interceptor.cc851
-rw-r--r--chromium/chrome/browser/signin/dice_web_signin_interceptor.h411
-rw-r--r--chromium/chrome/browser/signin/dice_web_signin_interceptor_browsertest.cc1200
-rw-r--r--chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.cc45
-rw-r--r--chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.h37
-rw-r--r--chromium/chrome/browser/signin/dice_web_signin_interceptor_unittest.cc953
-rw-r--r--chromium/chrome/browser/signin/e2e_tests/live_sign_in_test.cc776
-rw-r--r--chromium/chrome/browser/signin/e2e_tests/live_test.cc61
-rw-r--r--chromium/chrome/browser/signin/e2e_tests/live_test.h33
-rw-r--r--chromium/chrome/browser/signin/e2e_tests/test_accounts_util.cc75
-rw-r--r--chromium/chrome/browser/signin/e2e_tests/test_accounts_util.h46
-rw-r--r--chromium/chrome/browser/signin/e2e_tests/test_accounts_util_unittest.cc100
-rw-r--r--chromium/chrome/browser/signin/force_signin_verifier.cc195
-rw-r--r--chromium/chrome/browser/signin/force_signin_verifier.h95
-rw-r--r--chromium/chrome/browser/signin/force_signin_verifier_unittest.cc416
-rw-r--r--chromium/chrome/browser/signin/header_modification_delegate.h38
-rw-r--r--chromium/chrome/browser/signin/header_modification_delegate_impl.cc154
-rw-r--r--chromium/chrome/browser/signin/header_modification_delegate_impl.h68
-rw-r--r--chromium/chrome/browser/signin/identity_manager_factory.cc171
-rw-r--r--chromium/chrome/browser/signin/identity_manager_factory.h67
-rw-r--r--chromium/chrome/browser/signin/identity_manager_provider.cc38
-rw-r--r--chromium/chrome/browser/signin/identity_manager_provider.h32
-rw-r--r--chromium/chrome/browser/signin/identity_services_provider_android.cc39
-rw-r--r--chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.cc103
-rw-r--r--chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.h101
-rw-r--r--chromium/chrome/browser/signin/investigator_dependency_provider.cc14
-rw-r--r--chromium/chrome/browser/signin/investigator_dependency_provider.h33
-rw-r--r--chromium/chrome/browser/signin/logout_tab_helper.cc34
-rw-r--r--chromium/chrome/browser/signin/logout_tab_helper.h36
-rw-r--r--chromium/chrome/browser/signin/logout_tab_helper_unittest.cc24
-rw-r--r--chromium/chrome/browser/signin/mirror_browsertest.cc277
-rw-r--r--chromium/chrome/browser/signin/process_dice_header_delegate_impl.cc146
-rw-r--r--chromium/chrome/browser/signin/process_dice_header_delegate_impl.h77
-rw-r--r--chromium/chrome/browser/signin/process_dice_header_delegate_impl_unittest.cc375
-rw-r--r--chromium/chrome/browser/signin/reauth_result.h38
-rw-r--r--chromium/chrome/browser/signin/reauth_tab_helper.cc96
-rw-r--r--chromium/chrome/browser/signin/reauth_tab_helper.h66
-rw-r--r--chromium/chrome/browser/signin/reauth_tab_helper_unittest.cc184
-rw-r--r--chromium/chrome/browser/signin/reauth_util.cc40
-rw-r--r--chromium/chrome/browser/signin/reauth_util.h24
-rw-r--r--chromium/chrome/browser/signin/reauth_util_unittest.cc33
-rw-r--r--chromium/chrome/browser/signin/remove_local_account_browsertest.cc143
-rw-r--r--chromium/chrome/browser/signin/services/DIR_METADATA1
-rw-r--r--chromium/chrome/browser/signin/services/OWNERS2
-rw-r--r--chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/ProfileDataCacheUnitTest.java120
-rw-r--r--chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/WebSigninBridgeTest.java89
-rw-r--r--chromium/chrome/browser/signin/signin_error_controller_factory.cc48
-rw-r--r--chromium/chrome/browser/signin/signin_error_controller_factory.h37
-rw-r--r--chromium/chrome/browser/signin/signin_features.cc16
-rw-r--r--chromium/chrome/browser/signin/signin_features.h15
-rw-r--r--chromium/chrome/browser/signin/signin_global_error.cc170
-rw-r--r--chromium/chrome/browser/signin/signin_global_error.h64
-rw-r--r--chromium/chrome/browser/signin/signin_global_error_factory.cc46
-rw-r--r--chromium/chrome/browser/signin/signin_global_error_factory.h40
-rw-r--r--chromium/chrome/browser/signin/signin_global_error_unittest.cc166
-rw-r--r--chromium/chrome/browser/signin/signin_manager.cc200
-rw-r--r--chromium/chrome/browser/signin/signin_manager.h71
-rw-r--r--chromium/chrome/browser/signin/signin_manager_android_factory.cc41
-rw-r--r--chromium/chrome/browser/signin/signin_manager_android_factory.h32
-rw-r--r--chromium/chrome/browser/signin/signin_manager_factory.cc48
-rw-r--r--chromium/chrome/browser/signin/signin_manager_factory.h33
-rw-r--r--chromium/chrome/browser/signin/signin_manager_unittest.cc421
-rw-r--r--chromium/chrome/browser/signin/signin_profile_attributes_updater.cc72
-rw-r--r--chromium/chrome/browser/signin/signin_profile_attributes_updater.h56
-rw-r--r--chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.cc53
-rw-r--r--chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.h42
-rw-r--r--chromium/chrome/browser/signin/signin_profile_attributes_updater_unittest.cc224
-rw-r--r--chromium/chrome/browser/signin/signin_promo.cc143
-rw-r--r--chromium/chrome/browser/signin/signin_promo.h83
-rw-r--r--chromium/chrome/browser/signin/signin_promo_unittest.cc56
-rw-r--r--chromium/chrome/browser/signin/signin_promo_util.cc49
-rw-r--r--chromium/chrome/browser/signin/signin_promo_util.h18
-rw-r--r--chromium/chrome/browser/signin/signin_ui_util.cc608
-rw-r--r--chromium/chrome/browser/signin/signin_ui_util.h188
-rw-r--r--chromium/chrome/browser/signin/signin_ui_util_browsertest.cc85
-rw-r--r--chromium/chrome/browser/signin/signin_ui_util_unittest.cc736
-rw-r--r--chromium/chrome/browser/signin/signin_util.cc382
-rw-r--r--chromium/chrome/browser/signin/signin_util.h73
-rw-r--r--chromium/chrome/browser/signin/signin_util_unittest.cc127
-rw-r--r--chromium/chrome/browser/signin/signin_util_win.cc332
-rw-r--r--chromium/chrome/browser/signin/signin_util_win.h31
-rw-r--r--chromium/chrome/browser/signin/signin_util_win_browsertest.cc698
-rw-r--r--chromium/chrome/browser/signin/test_signin_client_builder.cc18
-rw-r--r--chromium/chrome/browser/signin/test_signin_client_builder.h26
-rw-r--r--chromium/chrome/browser/signin/token_revoker_test_utils.cc36
-rw-r--r--chromium/chrome/browser/signin/token_revoker_test_utils.h42
-rw-r--r--chromium/components/gcm_driver/DEPS27
-rw-r--r--chromium/components/gcm_driver/DIR_METADATA4
-rw-r--r--chromium/components/gcm_driver/OWNERS2
-rw-r--r--chromium/components/gcm_driver/account_tracker.cc147
-rw-r--r--chromium/components/gcm_driver/account_tracker.h78
-rw-r--r--chromium/components/gcm_driver/account_tracker_unittest.cc539
-rw-r--r--chromium/components/gcm_driver/android/OWNERS2
-rw-r--r--chromium/components/gcm_driver/android/junit/src/org/chromium/components/gcm_driver/GCMMessageTest.java267
-rw-r--r--chromium/components/gcm_driver/android/junit/src/org/chromium/components/gcm_driver/LazySubscriptionsManagerTest.java267
-rw-r--r--chromium/components/gcm_driver/common/gcm_driver_export.h29
-rw-r--r--chromium/components/gcm_driver/common/gcm_message.cc29
-rw-r--r--chromium/components/gcm_driver/common/gcm_message.h56
-rw-r--r--chromium/components/gcm_driver/crypto/DEPS7
-rw-r--r--chromium/components/gcm_driver/crypto/encryption_header_parsers.cc156
-rw-r--r--chromium/components/gcm_driver/crypto/encryption_header_parsers.h92
-rw-r--r--chromium/components/gcm_driver/crypto/encryption_header_parsers_unittest.cc374
-rw-r--r--chromium/components/gcm_driver/crypto/gcm_crypto_test_helpers.cc84
-rw-r--r--chromium/components/gcm_driver/crypto/gcm_crypto_test_helpers.h25
-rw-r--r--chromium/components/gcm_driver/crypto/gcm_decryption_result.cc50
-rw-r--r--chromium/components/gcm_driver/crypto/gcm_decryption_result.h71
-rw-r--r--chromium/components/gcm_driver/crypto/gcm_encryption_provider.cc412
-rw-r--r--chromium/components/gcm_driver/crypto/gcm_encryption_provider.h157
-rw-r--r--chromium/components/gcm_driver/crypto/gcm_encryption_provider_unittest.cc672
-rw-r--r--chromium/components/gcm_driver/crypto/gcm_encryption_result.h33
-rw-r--r--chromium/components/gcm_driver/crypto/gcm_key_store.cc425
-rw-r--r--chromium/components/gcm_driver/crypto/gcm_key_store.h150
-rw-r--r--chromium/components/gcm_driver/crypto/gcm_key_store_unittest.cc775
-rw-r--r--chromium/components/gcm_driver/crypto/gcm_message_cryptographer.cc477
-rw-r--r--chromium/components/gcm_driver/crypto/gcm_message_cryptographer.h167
-rw-r--r--chromium/components/gcm_driver/crypto/gcm_message_cryptographer_unittest.cc906
-rw-r--r--chromium/components/gcm_driver/crypto/message_payload_parser.cc77
-rw-r--r--chromium/components/gcm_driver/crypto/message_payload_parser.h97
-rw-r--r--chromium/components/gcm_driver/crypto/message_payload_parser_unittest.cc124
-rw-r--r--chromium/components/gcm_driver/crypto/p256_key_util.cc95
-rw-r--r--chromium/components/gcm_driver/crypto/p256_key_util.h43
-rw-r--r--chromium/components/gcm_driver/crypto/p256_key_util_unittest.cc158
-rw-r--r--chromium/components/gcm_driver/crypto/proto/gcm_encryption_data.proto58
-rw-r--r--chromium/components/gcm_driver/fake_gcm_app_handler.cc85
-rw-r--r--chromium/components/gcm_driver/fake_gcm_app_handler.h75
-rw-r--r--chromium/components/gcm_driver/fake_gcm_client.cc335
-rw-r--r--chromium/components/gcm_driver/fake_gcm_client.h139
-rw-r--r--chromium/components/gcm_driver/fake_gcm_client_factory.cc28
-rw-r--r--chromium/components/gcm_driver/fake_gcm_client_factory.h41
-rw-r--r--chromium/components/gcm_driver/fake_gcm_driver.cc110
-rw-r--r--chromium/components/gcm_driver/fake_gcm_driver.h71
-rw-r--r--chromium/components/gcm_driver/fake_gcm_profile_service.cc246
-rw-r--r--chromium/components/gcm_driver/fake_gcm_profile_service.h79
-rw-r--r--chromium/components/gcm_driver/features.cc41
-rw-r--r--chromium/components/gcm_driver/features.h24
-rw-r--r--chromium/components/gcm_driver/gcm_account_mapper.cc386
-rw-r--r--chromium/components/gcm_driver/gcm_account_mapper.h136
-rw-r--r--chromium/components/gcm_driver/gcm_account_mapper_unittest.cc955
-rw-r--r--chromium/components/gcm_driver/gcm_account_tracker.cc343
-rw-r--r--chromium/components/gcm_driver/gcm_account_tracker.h172
-rw-r--r--chromium/components/gcm_driver/gcm_account_tracker_unittest.cc534
-rw-r--r--chromium/components/gcm_driver/gcm_activity.cc62
-rw-r--r--chromium/components/gcm_driver/gcm_activity.h90
-rw-r--r--chromium/components/gcm_driver/gcm_app_handler.cc21
-rw-r--r--chromium/components/gcm_driver/gcm_app_handler.h67
-rw-r--r--chromium/components/gcm_driver/gcm_backoff_policy.cc46
-rw-r--r--chromium/components/gcm_driver/gcm_backoff_policy.h17
-rw-r--r--chromium/components/gcm_driver/gcm_client.cc38
-rw-r--r--chromium/components/gcm_driver/gcm_client.h366
-rw-r--r--chromium/components/gcm_driver/gcm_client_factory.cc23
-rw-r--r--chromium/components/gcm_driver/gcm_client_factory.h30
-rw-r--r--chromium/components/gcm_driver/gcm_client_impl.cc1481
-rw-r--r--chromium/components/gcm_driver/gcm_client_impl.h430
-rw-r--r--chromium/components/gcm_driver/gcm_client_impl_unittest.cc1966
-rw-r--r--chromium/components/gcm_driver/gcm_connection_observer.cc18
-rw-r--r--chromium/components/gcm_driver/gcm_connection_observer.h34
-rw-r--r--chromium/components/gcm_driver/gcm_delayed_task_controller.cc39
-rw-r--r--chromium/components/gcm_driver/gcm_delayed_task_controller.h44
-rw-r--r--chromium/components/gcm_driver/gcm_delayed_task_controller_unittest.cc63
-rw-r--r--chromium/components/gcm_driver/gcm_desktop_utils.cc101
-rw-r--r--chromium/components/gcm_driver/gcm_desktop_utils.h49
-rw-r--r--chromium/components/gcm_driver/gcm_driver.cc373
-rw-r--r--chromium/components/gcm_driver/gcm_driver.h395
-rw-r--r--chromium/components/gcm_driver/gcm_driver_android.cc275
-rw-r--r--chromium/components/gcm_driver/gcm_driver_android.h115
-rw-r--r--chromium/components/gcm_driver/gcm_driver_constants.cc15
-rw-r--r--chromium/components/gcm_driver/gcm_driver_constants.h20
-rw-r--r--chromium/components/gcm_driver/gcm_driver_desktop.cc1333
-rw-r--r--chromium/components/gcm_driver/gcm_driver_desktop.h259
-rw-r--r--chromium/components/gcm_driver/gcm_driver_desktop_unittest.cc1139
-rw-r--r--chromium/components/gcm_driver/gcm_driver_unittest.cc269
-rw-r--r--chromium/components/gcm_driver/gcm_internals_constants.cc41
-rw-r--r--chromium/components/gcm_driver/gcm_internals_constants.h47
-rw-r--r--chromium/components/gcm_driver/gcm_internals_helper.cc190
-rw-r--r--chromium/components/gcm_driver/gcm_internals_helper.h30
-rw-r--r--chromium/components/gcm_driver/gcm_profile_service.cc205
-rw-r--r--chromium/components/gcm_driver/gcm_profile_service.h111
-rw-r--r--chromium/components/gcm_driver/gcm_stats_recorder_android.cc148
-rw-r--r--chromium/components/gcm_driver/gcm_stats_recorder_android.h99
-rw-r--r--chromium/components/gcm_driver/gcm_stats_recorder_android_unittest.cc116
-rw-r--r--chromium/components/gcm_driver/gcm_stats_recorder_impl.cc514
-rw-r--r--chromium/components/gcm_driver/gcm_stats_recorder_impl.h171
-rw-r--r--chromium/components/gcm_driver/gcm_stats_recorder_impl_unittest.cc536
-rw-r--r--chromium/components/gcm_driver/instance_id/DEPS4
-rw-r--r--chromium/components/gcm_driver/instance_id/android/javatests/src/org/chromium/components/gcm_driver/instance_id/FakeInstanceIDWithSubtype.java198
-rw-r--r--chromium/components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.cc121
-rw-r--r--chromium/components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.h80
-rw-r--r--chromium/components/gcm_driver/instance_id/instance_id.cc58
-rw-r--r--chromium/components/gcm_driver/instance_id/instance_id.h182
-rw-r--r--chromium/components/gcm_driver/instance_id/instance_id_android.cc229
-rw-r--r--chromium/components/gcm_driver/instance_id/instance_id_android.h104
-rw-r--r--chromium/components/gcm_driver/instance_id/instance_id_driver.cc40
-rw-r--r--chromium/components/gcm_driver/instance_id/instance_id_driver.h58
-rw-r--r--chromium/components/gcm_driver/instance_id/instance_id_driver_unittest.cc366
-rw-r--r--chromium/components/gcm_driver/instance_id/instance_id_impl.cc275
-rw-r--r--chromium/components/gcm_driver/instance_id/instance_id_impl.h98
-rw-r--r--chromium/components/gcm_driver/instance_id/instance_id_profile_service.cc23
-rw-r--r--chromium/components/gcm_driver/instance_id/instance_id_profile_service.h38
-rw-r--r--chromium/components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.cc25
-rw-r--r--chromium/components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h31
-rw-r--r--chromium/components/gcm_driver/registration_info.cc286
-rw-r--r--chromium/components/gcm_driver/registration_info.h137
-rw-r--r--chromium/components/gcm_driver/resources/OWNERS2
-rw-r--r--chromium/components/gcm_driver/resources/gcm_internals.css47
-rw-r--r--chromium/components/gcm_driver/resources/gcm_internals.html210
-rw-r--r--chromium/components/gcm_driver/resources/gcm_internals.js222
-rw-r--r--chromium/components/gcm_driver/system_encryptor.cc23
-rw-r--r--chromium/components/gcm_driver/system_encryptor.h27
301 files changed, 57748 insertions, 0 deletions
diff --git a/chromium/chrome/browser/gcm/COMMON_METADATA b/chromium/chrome/browser/gcm/COMMON_METADATA
new file mode 100644
index 00000000000..777cdb6a192
--- /dev/null
+++ b/chromium/chrome/browser/gcm/COMMON_METADATA
@@ -0,0 +1,4 @@
+monorail: {
+ component: "Services>CloudMessaging"
+}
+team_email: "platform-capabilities@chromium.org"
diff --git a/chromium/chrome/browser/gcm/DIR_METADATA b/chromium/chrome/browser/gcm/DIR_METADATA
new file mode 100644
index 00000000000..66fe8683773
--- /dev/null
+++ b/chromium/chrome/browser/gcm/DIR_METADATA
@@ -0,0 +1 @@
+mixins: "//chrome/browser/gcm/COMMON_METADATA"
diff --git a/chromium/chrome/browser/gcm/OWNERS b/chromium/chrome/browser/gcm/OWNERS
new file mode 100644
index 00000000000..06f7d3cbe88
--- /dev/null
+++ b/chromium/chrome/browser/gcm/OWNERS
@@ -0,0 +1,4 @@
+dimich@chromium.org
+fgorski@chromium.org
+jianli@chromium.org
+peter@chromium.org
diff --git a/chromium/chrome/browser/gcm/gcm_product_util.cc b/chromium/chrome/browser/gcm/gcm_product_util.cc
new file mode 100644
index 00000000000..cb2fa486c11
--- /dev/null
+++ b/chromium/chrome/browser/gcm/gcm_product_util.cc
@@ -0,0 +1,57 @@
+// Copyright 2016 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.
+
+#include "chrome/browser/gcm/gcm_product_util.h"
+
+#include "base/strings/string_piece.h"
+#include "base/strings/string_util.h"
+#include "chrome/common/chrome_version.h"
+#include "chrome/common/pref_names.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/prefs/pref_registry_simple.h"
+#include "components/prefs/pref_service.h"
+#include "components/version_info/version_info.h"
+
+namespace gcm {
+
+namespace {
+
+std::string ToLowerAlphaNum(base::StringPiece in) {
+ std::string out;
+ out.reserve(in.size());
+ for (char ch : in) {
+ if (base::IsAsciiAlpha(ch) || base::IsAsciiDigit(ch))
+ out.push_back(base::ToLowerASCII(ch));
+ }
+ return out;
+}
+
+} // namespace
+
+std::string GetProductCategoryForSubtypes(PrefService* prefs) {
+ std::string product_category_for_subtypes =
+ prefs->GetString(prefs::kGCMProductCategoryForSubtypes);
+ if (!product_category_for_subtypes.empty())
+ return product_category_for_subtypes;
+
+ std::string product = ToLowerAlphaNum(PRODUCT_SHORTNAME_STRING);
+ std::string ns = product == "chromium" ? "org" : "com";
+ std::string platform = ToLowerAlphaNum(version_info::GetOSType());
+ product_category_for_subtypes = ns + '.' + product + '.' + platform;
+
+ prefs->SetString(prefs::kGCMProductCategoryForSubtypes,
+ product_category_for_subtypes);
+ return product_category_for_subtypes;
+}
+
+void RegisterPrefs(PrefRegistrySimple* registry) {
+ registry->RegisterStringPref(prefs::kGCMProductCategoryForSubtypes,
+ std::string() /* default_value */);
+}
+
+void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) {
+ RegisterPrefs(registry);
+}
+
+} // namespace gcm
diff --git a/chromium/chrome/browser/gcm/gcm_product_util.h b/chromium/chrome/browser/gcm/gcm_product_util.h
new file mode 100644
index 00000000000..f91c869b310
--- /dev/null
+++ b/chromium/chrome/browser/gcm/gcm_product_util.h
@@ -0,0 +1,30 @@
+// Copyright 2016 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_GCM_GCM_PRODUCT_UTIL_H_
+#define CHROME_BROWSER_GCM_GCM_PRODUCT_UTIL_H_
+
+#include <string>
+
+class PrefRegistrySimple;
+class PrefService;
+
+namespace user_prefs {
+class PrefRegistrySyncable;
+}
+
+namespace gcm {
+
+// Returns a string like "com.chrome.macosx" that should be used as the GCM
+// category when an app_id is sent as a subtype instead of as a category. This
+// is generated once, then remains fixed forever (even if the product name
+// changes), since it must match existing Instance ID tokens.
+std::string GetProductCategoryForSubtypes(PrefService* prefs);
+
+void RegisterPrefs(PrefRegistrySimple* registry);
+void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);
+
+} // namespace gcm
+
+#endif // CHROME_BROWSER_GCM_GCM_PRODUCT_UTIL_H_
diff --git a/chromium/chrome/browser/gcm/gcm_profile_service_factory.cc b/chromium/chrome/browser/gcm/gcm_profile_service_factory.cc
new file mode 100644
index 00000000000..b3207963fdb
--- /dev/null
+++ b/chromium/chrome/browser/gcm/gcm_profile_service_factory.cc
@@ -0,0 +1,174 @@
+// Copyright (c) 2013 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.
+
+#include "chrome/browser/gcm/gcm_profile_service_factory.h"
+#include <memory>
+
+#include "base/bind.h"
+#include "base/no_destructor.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/task/thread_pool.h"
+#include "build/build_config.h"
+#include "chrome/browser/profiles/incognito_helpers.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_key.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/gcm_driver/gcm_profile_service.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/offline_pages/buildflags/buildflags.h"
+#include "content/public/browser/browser_task_traits.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/network_service_instance.h"
+#include "content/public/browser/storage_partition.h"
+#include "services/network/public/mojom/network_context.mojom.h"
+
+#if !defined(OS_ANDROID)
+#include "chrome/browser/gcm/gcm_product_util.h"
+#include "chrome/common/channel_info.h"
+#include "components/gcm_driver/gcm_client_factory.h"
+#include "content/public/browser/browser_context.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+#endif
+
+#if BUILDFLAG(ENABLE_OFFLINE_PAGES)
+#include "chrome/browser/offline_pages/prefetch/prefetch_service_factory.h"
+#include "components/gcm_driver/gcm_driver.h"
+#include "components/offline_pages/core/offline_page_feature.h"
+#include "components/offline_pages/core/prefetch/prefetch_gcm_app_handler.h"
+#include "components/offline_pages/core/prefetch/prefetch_service.h"
+#endif
+
+namespace gcm {
+
+namespace {
+
+#if !defined(OS_ANDROID)
+// Requests a ProxyResolvingSocketFactory on the UI thread. Note that a WeakPtr
+// of GCMProfileService is needed to detect when the KeyedService shuts down,
+// and avoid calling into |profile| which might have also been destroyed.
+void RequestProxyResolvingSocketFactoryOnUIThread(
+ Profile* profile,
+ base::WeakPtr<GCMProfileService> service,
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>
+ receiver) {
+ if (!service)
+ return;
+ network::mojom::NetworkContext* network_context =
+ profile->GetDefaultStoragePartition()->GetNetworkContext();
+ network_context->CreateProxyResolvingSocketFactory(std::move(receiver));
+}
+
+// A thread-safe wrapper to request a ProxyResolvingSocketFactory.
+void RequestProxyResolvingSocketFactory(
+ Profile* profile,
+ base::WeakPtr<GCMProfileService> service,
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>
+ receiver) {
+ content::GetUIThreadTaskRunner({})->PostTask(
+ FROM_HERE,
+ base::BindOnce(&RequestProxyResolvingSocketFactoryOnUIThread, profile,
+ std::move(service), std::move(receiver)));
+}
+#endif
+
+BrowserContextKeyedServiceFactory::TestingFactory& GetTestingFactory() {
+ static base::NoDestructor<BrowserContextKeyedServiceFactory::TestingFactory>
+ testing_factory;
+ return *testing_factory;
+}
+
+} // namespace
+
+GCMProfileServiceFactory::ScopedTestingFactoryInstaller::
+ ScopedTestingFactoryInstaller(TestingFactory testing_factory) {
+ DCHECK(!GetTestingFactory());
+ GetTestingFactory() = std::move(testing_factory);
+}
+
+GCMProfileServiceFactory::ScopedTestingFactoryInstaller::
+ ~ScopedTestingFactoryInstaller() {
+ GetTestingFactory() = BrowserContextKeyedServiceFactory::TestingFactory();
+}
+
+// static
+GCMProfileService* GCMProfileServiceFactory::GetForProfile(
+ content::BrowserContext* profile) {
+ // GCM is not supported in incognito mode.
+ if (profile->IsOffTheRecord())
+ return NULL;
+
+ return static_cast<GCMProfileService*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+GCMProfileServiceFactory* GCMProfileServiceFactory::GetInstance() {
+ static base::NoDestructor<GCMProfileServiceFactory> instance;
+ return instance.get();
+}
+
+GCMProfileServiceFactory::GCMProfileServiceFactory()
+ : BrowserContextKeyedServiceFactory(
+ "GCMProfileService",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(IdentityManagerFactory::GetInstance());
+#if BUILDFLAG(ENABLE_OFFLINE_PAGES)
+ DependsOn(offline_pages::PrefetchServiceFactory::GetInstance());
+#endif // BUILDFLAG(ENABLE_OFFLINE_PAGES)
+}
+
+GCMProfileServiceFactory::~GCMProfileServiceFactory() {
+}
+
+KeyedService* GCMProfileServiceFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ DCHECK(!profile->IsOffTheRecord());
+
+ TestingFactory& testing_factory = GetTestingFactory();
+ if (testing_factory)
+ return testing_factory.Run(context).release();
+
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner(
+ base::ThreadPool::CreateSequencedTaskRunner(
+ {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
+ base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}));
+ std::unique_ptr<GCMProfileService> service;
+#if defined(OS_ANDROID)
+ service = std::make_unique<GCMProfileService>(profile->GetPath(),
+ blocking_task_runner);
+#else
+ service = std::make_unique<GCMProfileService>(
+ profile->GetPrefs(), profile->GetPath(),
+ base::BindRepeating(&RequestProxyResolvingSocketFactory, profile),
+ profile->GetDefaultStoragePartition()
+ ->GetURLLoaderFactoryForBrowserProcess(),
+ content::GetNetworkConnectionTracker(), chrome::GetChannel(),
+ gcm::GetProductCategoryForSubtypes(profile->GetPrefs()),
+ IdentityManagerFactory::GetForProfile(profile),
+ std::make_unique<GCMClientFactory>(), content::GetUIThreadTaskRunner({}),
+ content::GetIOThreadTaskRunner({}), blocking_task_runner);
+#endif
+#if BUILDFLAG(ENABLE_OFFLINE_PAGES)
+ offline_pages::PrefetchService* prefetch_service =
+ offline_pages::PrefetchServiceFactory::GetForKey(
+ profile->GetProfileKey());
+ if (prefetch_service != nullptr) {
+ offline_pages::PrefetchGCMHandler* prefetch_gcm_handler =
+ prefetch_service->GetPrefetchGCMHandler();
+ service->driver()->AddAppHandler(prefetch_gcm_handler->GetAppId(),
+ prefetch_gcm_handler->AsGCMAppHandler());
+ }
+#endif // BUILDFLAG(ENABLE_OFFLINE_PAGES)
+
+ return service.release();
+}
+
+content::BrowserContext* GCMProfileServiceFactory::GetBrowserContextToUse(
+ content::BrowserContext* context) const {
+ return chrome::GetBrowserContextOwnInstanceInIncognito(context);
+}
+
+} // namespace gcm
diff --git a/chromium/chrome/browser/gcm/gcm_profile_service_factory.h b/chromium/chrome/browser/gcm/gcm_profile_service_factory.h
new file mode 100644
index 00000000000..13511e84611
--- /dev/null
+++ b/chromium/chrome/browser/gcm/gcm_profile_service_factory.h
@@ -0,0 +1,57 @@
+// Copyright (c) 2013 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_GCM_GCM_PROFILE_SERVICE_FACTORY_H_
+#define CHROME_BROWSER_GCM_GCM_PROFILE_SERVICE_FACTORY_H_
+
+#include "base/no_destructor.h"
+#include "components/gcm_driver/system_encryptor.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+namespace gcm {
+
+class GCMProfileService;
+
+// Singleton that owns all GCMProfileService and associates them with
+// Profiles.
+class GCMProfileServiceFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ static GCMProfileService* GetForProfile(content::BrowserContext* profile);
+ static GCMProfileServiceFactory* GetInstance();
+
+ // Helper registering a testing factory. Needs to be instantiated before the
+ // factory is accessed in your test, and deallocated after the last access.
+ // Usually this is achieved by putting this object as the first member in
+ // your test fixture.
+ class ScopedTestingFactoryInstaller {
+ public:
+ explicit ScopedTestingFactoryInstaller(TestingFactory testing_factory);
+
+ ScopedTestingFactoryInstaller(const ScopedTestingFactoryInstaller&) =
+ delete;
+ ScopedTestingFactoryInstaller& operator=(
+ const ScopedTestingFactoryInstaller&) = delete;
+
+ ~ScopedTestingFactoryInstaller();
+ };
+
+ GCMProfileServiceFactory(const GCMProfileServiceFactory&) = delete;
+ GCMProfileServiceFactory& operator=(const GCMProfileServiceFactory&) = delete;
+
+ private:
+ friend base::NoDestructor<GCMProfileServiceFactory>;
+
+ GCMProfileServiceFactory();
+ ~GCMProfileServiceFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+ content::BrowserContext* GetBrowserContextToUse(
+ content::BrowserContext* context) const override;
+};
+
+} // namespace gcm
+
+#endif // CHROME_BROWSER_GCM_GCM_PROFILE_SERVICE_FACTORY_H_
diff --git a/chromium/chrome/browser/gcm/gcm_profile_service_unittest.cc b/chromium/chrome/browser/gcm/gcm_profile_service_unittest.cc
new file mode 100644
index 00000000000..2a13932a454
--- /dev/null
+++ b/chromium/chrome/browser/gcm/gcm_profile_service_unittest.cc
@@ -0,0 +1,269 @@
+// Copyright 2014 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.
+
+#include <memory>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/callback_helpers.h"
+#include "base/memory/raw_ptr.h"
+#include "base/run_loop.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/task/thread_pool.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/gcm/gcm_product_util.h"
+#include "chrome/browser/gcm/gcm_profile_service_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/common/channel_info.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/gcm_driver/fake_gcm_app_handler.h"
+#include "components/gcm_driver/fake_gcm_client.h"
+#include "components/gcm_driver/fake_gcm_client_factory.h"
+#include "components/gcm_driver/gcm_client.h"
+#include "components/gcm_driver/gcm_client_factory.h"
+#include "components/gcm_driver/gcm_driver.h"
+#include "components/gcm_driver/gcm_profile_service.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/browser_task_traits.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/storage_partition.h"
+#include "content/public/test/browser_task_environment.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+#include "services/network/public/mojom/network_context.mojom.h"
+#include "services/network/test/test_network_connection_tracker.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chromeos/dbus/concierge/concierge_client.h"
+#endif
+
+namespace gcm {
+
+namespace {
+
+const char kTestAppID[] = "TestApp";
+const char kUserID[] = "user";
+
+void RequestProxyResolvingSocketFactoryOnUIThread(
+ Profile* profile,
+ base::WeakPtr<gcm::GCMProfileService> service,
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>
+ receiver) {
+ if (!service)
+ return;
+ return profile->GetDefaultStoragePartition()
+ ->GetNetworkContext()
+ ->CreateProxyResolvingSocketFactory(std::move(receiver));
+}
+
+void RequestProxyResolvingSocketFactory(
+ Profile* profile,
+ base::WeakPtr<gcm::GCMProfileService> service,
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>
+ receiver) {
+ content::GetUIThreadTaskRunner({})->PostTask(
+ FROM_HERE, base::BindOnce(&RequestProxyResolvingSocketFactoryOnUIThread,
+ profile, service, std::move(receiver)));
+}
+
+std::unique_ptr<KeyedService> BuildGCMProfileService(
+ content::BrowserContext* context) {
+ Profile* profile = Profile::FromBrowserContext(context);
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner(
+ base::ThreadPool::CreateSequencedTaskRunner(
+ {base::MayBlock(), base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}));
+ return std::make_unique<gcm::GCMProfileService>(
+ profile->GetPrefs(), profile->GetPath(),
+ base::BindRepeating(&RequestProxyResolvingSocketFactory, profile),
+ profile->GetDefaultStoragePartition()
+ ->GetURLLoaderFactoryForBrowserProcess(),
+ network::TestNetworkConnectionTracker::GetInstance(),
+ chrome::GetChannel(),
+ gcm::GetProductCategoryForSubtypes(profile->GetPrefs()),
+ IdentityManagerFactory::GetForProfile(profile),
+ std::unique_ptr<gcm::GCMClientFactory>(
+ new gcm::FakeGCMClientFactory(content::GetUIThreadTaskRunner({}),
+ content::GetIOThreadTaskRunner({}))),
+ content::GetUIThreadTaskRunner({}), content::GetIOThreadTaskRunner({}),
+ blocking_task_runner);
+}
+
+} // namespace
+
+class GCMProfileServiceTest : public testing::Test {
+ public:
+ GCMProfileServiceTest(const GCMProfileServiceTest&) = delete;
+ GCMProfileServiceTest& operator=(const GCMProfileServiceTest&) = delete;
+
+ protected:
+ GCMProfileServiceTest();
+ ~GCMProfileServiceTest() override;
+
+ // testing::Test:
+ void SetUp() override;
+ void TearDown() override;
+
+ FakeGCMClient* GetGCMClient() const;
+
+ void CreateGCMProfileService();
+
+ void RegisterAndWaitForCompletion(const std::vector<std::string>& sender_ids);
+ void UnregisterAndWaitForCompletion();
+ void SendAndWaitForCompletion(const OutgoingMessage& message);
+
+ void RegisterCompleted(base::OnceClosure callback,
+ const std::string& registration_id,
+ GCMClient::Result result);
+ void UnregisterCompleted(base::OnceClosure callback,
+ GCMClient::Result result);
+ void SendCompleted(base::OnceClosure callback,
+ const std::string& message_id,
+ GCMClient::Result result);
+
+ GCMDriver* driver() const { return gcm_profile_service_->driver(); }
+ std::string registration_id() const { return registration_id_; }
+ GCMClient::Result registration_result() const { return registration_result_; }
+ GCMClient::Result unregistration_result() const {
+ return unregistration_result_;
+ }
+ std::string send_message_id() const { return send_message_id_; }
+ GCMClient::Result send_result() const { return send_result_; }
+
+ private:
+ content::BrowserTaskEnvironment task_environment_;
+ std::unique_ptr<TestingProfile> profile_;
+ raw_ptr<GCMProfileService> gcm_profile_service_;
+ std::unique_ptr<FakeGCMAppHandler> gcm_app_handler_;
+
+ std::string registration_id_;
+ GCMClient::Result registration_result_;
+ GCMClient::Result unregistration_result_;
+ std::string send_message_id_;
+ GCMClient::Result send_result_;
+};
+
+GCMProfileServiceTest::GCMProfileServiceTest()
+ : gcm_profile_service_(nullptr),
+ gcm_app_handler_(new FakeGCMAppHandler),
+ registration_result_(GCMClient::UNKNOWN_ERROR),
+ send_result_(GCMClient::UNKNOWN_ERROR) {}
+
+GCMProfileServiceTest::~GCMProfileServiceTest() {
+}
+
+FakeGCMClient* GCMProfileServiceTest::GetGCMClient() const {
+ return static_cast<FakeGCMClient*>(
+ gcm_profile_service_->driver()->GetGCMClientForTesting());
+}
+
+void GCMProfileServiceTest::SetUp() {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ chromeos::ConciergeClient::InitializeFake(/*fake_cicerone_client=*/nullptr);
+#endif
+ TestingProfile::Builder builder;
+ profile_ = builder.Build();
+}
+
+void GCMProfileServiceTest::TearDown() {
+ gcm_profile_service_->driver()->RemoveAppHandler(kTestAppID);
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ profile_.reset();
+ chromeos::ConciergeClient::Shutdown();
+#endif
+}
+
+void GCMProfileServiceTest::CreateGCMProfileService() {
+ gcm_profile_service_ = static_cast<GCMProfileService*>(
+ GCMProfileServiceFactory::GetInstance()->SetTestingFactoryAndUse(
+ profile_.get(), base::BindRepeating(&BuildGCMProfileService)));
+ gcm_profile_service_->driver()->AddAppHandler(
+ kTestAppID, gcm_app_handler_.get());
+}
+
+void GCMProfileServiceTest::RegisterAndWaitForCompletion(
+ const std::vector<std::string>& sender_ids) {
+ base::RunLoop run_loop;
+ gcm_profile_service_->driver()->Register(
+ kTestAppID, sender_ids,
+ base::BindOnce(&GCMProfileServiceTest::RegisterCompleted,
+ base::Unretained(this), run_loop.QuitClosure()));
+ run_loop.Run();
+}
+
+void GCMProfileServiceTest::UnregisterAndWaitForCompletion() {
+ base::RunLoop run_loop;
+ gcm_profile_service_->driver()->Unregister(
+ kTestAppID,
+ base::BindOnce(&GCMProfileServiceTest::UnregisterCompleted,
+ base::Unretained(this), run_loop.QuitClosure()));
+ run_loop.Run();
+}
+
+void GCMProfileServiceTest::SendAndWaitForCompletion(
+ const OutgoingMessage& message) {
+ base::RunLoop run_loop;
+ gcm_profile_service_->driver()->Send(
+ kTestAppID, kUserID, message,
+ base::BindOnce(&GCMProfileServiceTest::SendCompleted,
+ base::Unretained(this), run_loop.QuitClosure()));
+ run_loop.Run();
+}
+
+void GCMProfileServiceTest::RegisterCompleted(
+ base::OnceClosure callback,
+ const std::string& registration_id,
+ GCMClient::Result result) {
+ registration_id_ = registration_id;
+ registration_result_ = result;
+ std::move(callback).Run();
+}
+
+void GCMProfileServiceTest::UnregisterCompleted(base::OnceClosure callback,
+ GCMClient::Result result) {
+ unregistration_result_ = result;
+ std::move(callback).Run();
+}
+
+void GCMProfileServiceTest::SendCompleted(base::OnceClosure callback,
+ const std::string& message_id,
+ GCMClient::Result result) {
+ send_message_id_ = message_id;
+ send_result_ = result;
+ std::move(callback).Run();
+}
+
+TEST_F(GCMProfileServiceTest, RegisterAndUnregister) {
+ CreateGCMProfileService();
+
+ std::vector<std::string> sender_ids;
+ sender_ids.push_back("sender");
+ RegisterAndWaitForCompletion(sender_ids);
+
+ std::string expected_registration_id =
+ FakeGCMClient::GenerateGCMRegistrationID(sender_ids);
+ EXPECT_EQ(expected_registration_id, registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, registration_result());
+
+ UnregisterAndWaitForCompletion();
+ EXPECT_EQ(GCMClient::SUCCESS, unregistration_result());
+}
+
+TEST_F(GCMProfileServiceTest, Send) {
+ CreateGCMProfileService();
+
+ OutgoingMessage message;
+ message.id = "1";
+ message.data["key1"] = "value1";
+ SendAndWaitForCompletion(message);
+
+ EXPECT_EQ(message.id, send_message_id());
+ EXPECT_EQ(GCMClient::SUCCESS, send_result());
+}
+
+} // namespace gcm
diff --git a/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.cc b/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.cc
new file mode 100644
index 00000000000..8123d8f04c6
--- /dev/null
+++ b/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.cc
@@ -0,0 +1,60 @@
+// Copyright (c) 2015 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.
+
+#include "chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h"
+
+#include <memory>
+
+#include "chrome/browser/gcm/gcm_profile_service_factory.h"
+#include "chrome/browser/profiles/incognito_helpers.h"
+#include "chrome/browser/profiles/profile.h"
+#include "components/gcm_driver/gcm_profile_service.h"
+#include "components/gcm_driver/instance_id/instance_id_profile_service.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+
+namespace instance_id {
+
+// static
+InstanceIDProfileService* InstanceIDProfileServiceFactory::GetForProfile(
+ content::BrowserContext* profile) {
+ // Instance ID is not supported in incognito mode.
+ if (profile->IsOffTheRecord())
+ return NULL;
+
+ return static_cast<InstanceIDProfileService*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+InstanceIDProfileServiceFactory*
+InstanceIDProfileServiceFactory::GetInstance() {
+ return base::Singleton<InstanceIDProfileServiceFactory>::get();
+}
+
+InstanceIDProfileServiceFactory::InstanceIDProfileServiceFactory()
+ : BrowserContextKeyedServiceFactory(
+ "InstanceIDProfileService",
+ BrowserContextDependencyManager::GetInstance()) {
+ // GCM is needed for device ID.
+ DependsOn(gcm::GCMProfileServiceFactory::GetInstance());
+}
+
+InstanceIDProfileServiceFactory::~InstanceIDProfileServiceFactory() {
+}
+
+KeyedService* InstanceIDProfileServiceFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ return new InstanceIDProfileService(
+ gcm::GCMProfileServiceFactory::GetForProfile(profile)->driver(),
+ profile->IsOffTheRecord());
+}
+
+content::BrowserContext*
+InstanceIDProfileServiceFactory::GetBrowserContextToUse(
+ content::BrowserContext* context) const {
+ return chrome::GetBrowserContextOwnInstanceInIncognito(context);
+}
+
+} // namespace instance_id
diff --git a/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h b/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h
new file mode 100644
index 00000000000..c4505760a3d
--- /dev/null
+++ b/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h
@@ -0,0 +1,44 @@
+// Copyright (c) 2015 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_GCM_INSTANCE_ID_INSTANCE_ID_PROFILE_SERVICE_FACTORY_H_
+#define CHROME_BROWSER_GCM_INSTANCE_ID_INSTANCE_ID_PROFILE_SERVICE_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+namespace instance_id {
+
+class InstanceIDProfileService;
+
+// Singleton that owns all InstanceIDProfileService and associates them with
+// profiles.
+class InstanceIDProfileServiceFactory :
+ public BrowserContextKeyedServiceFactory {
+ public:
+ static InstanceIDProfileService* GetForProfile(
+ content::BrowserContext* profile);
+ static InstanceIDProfileServiceFactory* GetInstance();
+
+ InstanceIDProfileServiceFactory(const InstanceIDProfileServiceFactory&) =
+ delete;
+ InstanceIDProfileServiceFactory& operator=(
+ const InstanceIDProfileServiceFactory&) = delete;
+
+ private:
+ friend struct base::DefaultSingletonTraits<InstanceIDProfileServiceFactory>;
+
+ InstanceIDProfileServiceFactory();
+ ~InstanceIDProfileServiceFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+ content::BrowserContext* GetBrowserContextToUse(
+ content::BrowserContext* context) const override;
+};
+
+} // namespace instance_id
+
+#endif // CHROME_BROWSER_GCM_INSTANCE_ID_INSTANCE_ID_PROFILE_SERVICE_FACTORY_H_
diff --git a/chromium/chrome/browser/profiles/incognito_helpers.cc b/chromium/chrome/browser/profiles/incognito_helpers.cc
new file mode 100644
index 00000000000..a319237928b
--- /dev/null
+++ b/chromium/chrome/browser/profiles/incognito_helpers.cc
@@ -0,0 +1,28 @@
+// Copyright 2013 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.
+
+#include "chrome/browser/profiles/incognito_helpers.h"
+
+#include "chrome/browser/profiles/profile.h"
+
+namespace chrome {
+
+content::BrowserContext* GetBrowserContextRedirectedInIncognito(
+ content::BrowserContext* context) {
+ return Profile::FromBrowserContext(context)->GetOriginalProfile();
+}
+
+const content::BrowserContext* GetBrowserContextRedirectedInIncognito(
+ const content::BrowserContext* context) {
+ const Profile* profile = Profile::FromBrowserContext(
+ const_cast<content::BrowserContext*>(context));
+ return profile->GetOriginalProfile();
+}
+
+content::BrowserContext* GetBrowserContextOwnInstanceInIncognito(
+ content::BrowserContext* context) {
+ return context;
+}
+
+} // namespace chrome
diff --git a/chromium/chrome/browser/profiles/incognito_helpers.h b/chromium/chrome/browser/profiles/incognito_helpers.h
new file mode 100644
index 00000000000..e8e76ce5b95
--- /dev/null
+++ b/chromium/chrome/browser/profiles/incognito_helpers.h
@@ -0,0 +1,29 @@
+// Copyright 2013 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_PROFILES_INCOGNITO_HELPERS_H_
+#define CHROME_BROWSER_PROFILES_INCOGNITO_HELPERS_H_
+
+namespace content {
+class BrowserContext;
+}
+
+namespace chrome {
+
+// Returns the original browser context even for Incognito contexts.
+content::BrowserContext* GetBrowserContextRedirectedInIncognito(
+ content::BrowserContext* context);
+
+// Returns the original browser context even for Incognito contexts.
+const content::BrowserContext* GetBrowserContextRedirectedInIncognito(
+ const content::BrowserContext* context);
+
+// Returns non-NULL even for Incognito contexts so that a separate
+// instance of a service is created for the Incognito context.
+content::BrowserContext* GetBrowserContextOwnInstanceInIncognito(
+ content::BrowserContext* context);
+
+} // namespace chrome
+
+#endif // CHROME_BROWSER_PROFILES_INCOGNITO_HELPERS_H_
diff --git a/chromium/chrome/browser/push_messaging/DIR_METADATA b/chromium/chrome/browser/push_messaging/DIR_METADATA
new file mode 100644
index 00000000000..a684b81174a
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/DIR_METADATA
@@ -0,0 +1 @@
+mixins: "//content/browser/push_messaging/COMMON_METADATA"
diff --git a/chromium/chrome/browser/push_messaging/OWNERS b/chromium/chrome/browser/push_messaging/OWNERS
new file mode 100644
index 00000000000..d09ffef01de
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/OWNERS
@@ -0,0 +1 @@
+file://content/browser/push_messaging/OWNERS
diff --git a/chromium/chrome/browser/push_messaging/budget.proto b/chromium/chrome/browser/push_messaging/budget.proto
new file mode 100644
index 00000000000..229ea5370b7
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/budget.proto
@@ -0,0 +1,30 @@
+// Copyright 2016 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.
+
+syntax = "proto2";
+
+package budget_service;
+
+// Chrome requires this.
+option optimize_for = LITE_RUNTIME;
+
+// Next available id: 3
+message Budget {
+ // The sequence of budget chunks and their expiration times.
+ repeated BudgetChunk budget = 1;
+
+ // The timestamp of the last time that new engagement budget was awarded.
+ // This stores the internal value needed to construct a base::Time object.
+ optional int64 engagement_last_updated = 2;
+}
+
+// Next available id: 3
+message BudgetChunk {
+ // The amount of budget remaining in this chunk.
+ optional double amount = 1;
+
+ // The timestamp when the budget expires. This stores the internal value
+ // needed to construct a base::Time object.
+ optional int64 expiration = 2;
+}
diff --git a/chromium/chrome/browser/push_messaging/budget_database.cc b/chromium/chrome/browser/push_messaging/budget_database.cc
new file mode 100644
index 00000000000..96b2d36d01d
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/budget_database.cc
@@ -0,0 +1,400 @@
+// Copyright 2016 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.
+
+#include "chrome/browser/push_messaging/budget_database.h"
+
+#include "base/bind.h"
+#include "base/memory/ptr_util.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/task/post_task.h"
+#include "base/task/thread_pool.h"
+#include "base/time/clock.h"
+#include "base/time/default_clock.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/push_messaging/budget.pb.h"
+#include "components/leveldb_proto/public/proto_database_provider.h"
+#include "components/site_engagement/content/site_engagement_score.h"
+#include "components/site_engagement/content/site_engagement_service.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/storage_partition.h"
+#include "url/gurl.h"
+#include "url/origin.h"
+
+using content::BrowserThread;
+
+namespace {
+
+// The default amount of time during which a budget will be valid.
+constexpr int kBudgetDurationInDays = 4;
+
+// The amount of budget that a maximally engaged site should receive per hour.
+// For context, silent push messages cost 2 each, so this allows 6 silent push
+// messages a day for a fully engaged site. See budget_manager.cc for costs of
+// various actions.
+constexpr double kMaximumHourlyBudget = 12.0 / 24.0;
+
+} // namespace
+
+BudgetState::BudgetState() = default;
+BudgetState::BudgetState(const BudgetState& other) = default;
+BudgetState::~BudgetState() = default;
+
+BudgetState& BudgetState::operator=(const BudgetState& other) = default;
+
+BudgetDatabase::BudgetInfo::BudgetInfo() = default;
+
+BudgetDatabase::BudgetInfo::BudgetInfo(const BudgetInfo&& other)
+ : last_engagement_award(other.last_engagement_award) {
+ chunks = std::move(other.chunks);
+}
+
+BudgetDatabase::BudgetInfo::~BudgetInfo() = default;
+
+BudgetDatabase::BudgetDatabase(Profile* profile)
+ : profile_(profile), clock_(base::WrapUnique(new base::DefaultClock)) {
+ auto* protodb_provider =
+ profile->GetDefaultStoragePartition()->GetProtoDatabaseProvider();
+ // In incognito mode the provider service is not created.
+ if (!protodb_provider)
+ return;
+
+ db_ = protodb_provider->GetDB<budget_service::Budget>(
+ leveldb_proto::ProtoDbType::BUDGET_DATABASE,
+ profile->GetPath().Append(FILE_PATH_LITERAL("BudgetDatabase")),
+ base::ThreadPool::CreateSequencedTaskRunner(
+ {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
+ base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}));
+ db_->Init(base::BindOnce(&BudgetDatabase::OnDatabaseInit,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+BudgetDatabase::~BudgetDatabase() = default;
+
+void BudgetDatabase::GetBudgetDetails(const url::Origin& origin,
+ GetBudgetCallback callback) {
+ SyncCache(origin, base::BindOnce(&BudgetDatabase::GetBudgetAfterSync,
+ weak_ptr_factory_.GetWeakPtr(), origin,
+ std::move(callback)));
+}
+
+void BudgetDatabase::SpendBudget(const url::Origin& origin,
+ SpendBudgetCallback callback,
+ double amount) {
+ SyncCache(origin, base::BindOnce(&BudgetDatabase::SpendBudgetAfterSync,
+ weak_ptr_factory_.GetWeakPtr(), origin,
+ amount, std::move(callback)));
+}
+
+void BudgetDatabase::SetClockForTesting(std::unique_ptr<base::Clock> clock) {
+ clock_ = std::move(clock);
+}
+
+void BudgetDatabase::OnDatabaseInit(leveldb_proto::Enums::InitStatus status) {
+ // TODO(harkness): Consider caching the budget database now?
+ if (status != leveldb_proto::Enums::InitStatus::kOK)
+ db_.reset();
+}
+
+bool BudgetDatabase::IsCached(const url::Origin& origin) const {
+ return budget_map_.find(origin) != budget_map_.end();
+}
+
+double BudgetDatabase::GetBudget(const url::Origin& origin) const {
+ double total = 0;
+ auto iter = budget_map_.find(origin);
+ if (iter == budget_map_.end())
+ return total;
+
+ const BudgetInfo& info = iter->second;
+ for (const BudgetChunk& chunk : info.chunks)
+ total += chunk.amount;
+ return total;
+}
+
+void BudgetDatabase::AddToCache(
+ const url::Origin& origin,
+ CacheCallback callback,
+ bool success,
+ std::unique_ptr<budget_service::Budget> budget_proto) {
+ // If the database read failed or there's nothing to add, just return.
+ if (!success || !budget_proto) {
+ std::move(callback).Run(success);
+ return;
+ }
+
+ // If there were two simultaneous loads, don't overwrite the cache value,
+ // which might have been updated after the previous load.
+ if (IsCached(origin)) {
+ std::move(callback).Run(success);
+ return;
+ }
+
+ // Add the data to the cache, converting from the proto format to an STL
+ // format which is better for removing things from the list.
+ BudgetInfo& info = budget_map_[origin];
+ for (const auto& chunk : budget_proto->budget()) {
+ info.chunks.emplace_back(chunk.amount(),
+ base::Time::FromInternalValue(chunk.expiration()));
+ }
+
+ info.last_engagement_award =
+ base::Time::FromInternalValue(budget_proto->engagement_last_updated());
+
+ std::move(callback).Run(success);
+}
+
+void BudgetDatabase::GetBudgetAfterSync(const url::Origin& origin,
+ GetBudgetCallback callback,
+ bool success) {
+ std::vector<BudgetState> predictions;
+
+ // If the database wasn't able to read the information, return the
+ // failure and an empty predictions array.
+ if (!success) {
+ std::move(callback).Run(std::move(predictions));
+ return;
+ }
+
+ // Now, build up the BudgetExpection. This is different from the format
+ // in which the cache stores the data. The cache stores chunks of budget and
+ // when that budget expires. The mojo array describes a set of times
+ // and the budget at those times.
+ double total = GetBudget(origin);
+
+ // Always add one entry at the front of the list for the total budget now.
+ {
+ BudgetState prediction;
+ prediction.budget_at = total;
+ prediction.time = clock_->Now().ToJsTime();
+ predictions.push_back(prediction);
+ }
+
+ // Starting with the soonest expiring chunks, add entries for the
+ // expiration times going forward.
+ const BudgetChunks& chunks = budget_map_[origin].chunks;
+ for (const auto& chunk : chunks) {
+ BudgetState prediction;
+ total -= chunk.amount;
+ prediction.budget_at = total;
+ prediction.time = chunk.expiration.ToJsTime();
+ predictions.push_back(prediction);
+ }
+
+ std::move(callback).Run(std::move(predictions));
+}
+
+void BudgetDatabase::SpendBudgetAfterSync(const url::Origin& origin,
+ double amount,
+ SpendBudgetCallback callback,
+ bool success) {
+ if (!success) {
+ std::move(callback).Run(false /* success */);
+ return;
+ }
+
+ // Get the current SES score, to generate UMA.
+ double score = GetSiteEngagementScoreForOrigin(origin);
+
+ // Walk the list of budget chunks to see if the origin has enough budget.
+ double total = 0;
+ BudgetInfo& info = budget_map_[origin];
+ for (const BudgetChunk& chunk : info.chunks)
+ total += chunk.amount;
+
+ if (total < amount) {
+ UMA_HISTOGRAM_COUNTS_100("PushMessaging.SESForNoBudgetOrigin", score);
+ std::move(callback).Run(false /* success */);
+ return;
+ } else if (total < amount * 2) {
+ UMA_HISTOGRAM_COUNTS_100("PushMessaging.SESForLowBudgetOrigin", score);
+ }
+
+ // Walk the chunks and remove enough budget to cover the needed amount.
+ double bill = amount;
+ for (auto iter = info.chunks.begin(); iter != info.chunks.end();) {
+ if (iter->amount > bill) {
+ iter->amount -= bill;
+ bill = 0;
+ break;
+ }
+ bill -= iter->amount;
+ iter = info.chunks.erase(iter);
+ }
+
+ // There should have been enough budget to cover the entire bill.
+ DCHECK_EQ(0, bill);
+
+ // Now that the cache is updated, write the data to the database.
+ WriteCachedValuesToDatabase(
+ origin,
+ base::BindOnce(&BudgetDatabase::SpendBudgetAfterWrite,
+ weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+}
+
+// This converts the bool value which is returned from the database to a Mojo
+// error type.
+void BudgetDatabase::SpendBudgetAfterWrite(SpendBudgetCallback callback,
+ bool write_successful) {
+ // TODO(harkness): If the database write fails, the cache will be out of sync
+ // with the database. Consider ways to mitigate this.
+ if (!write_successful) {
+ std::move(callback).Run(false /* success */);
+ return;
+ }
+ std::move(callback).Run(true /* success */);
+}
+
+void BudgetDatabase::WriteCachedValuesToDatabase(const url::Origin& origin,
+ StoreBudgetCallback callback) {
+ if (!db_) {
+ base::SequencedTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::BindOnce(std::move(callback), false));
+ return;
+ }
+
+ // Create the data structures that are passed to the ProtoDatabase.
+ std::unique_ptr<
+ leveldb_proto::ProtoDatabase<budget_service::Budget>::KeyEntryVector>
+ entries(new leveldb_proto::ProtoDatabase<
+ budget_service::Budget>::KeyEntryVector());
+ std::unique_ptr<std::vector<std::string>> keys_to_remove(
+ new std::vector<std::string>());
+
+ // Each operation can either update the existing budget or remove the origin's
+ // budget information.
+ if (IsCached(origin)) {
+ // Build the Budget proto object.
+ budget_service::Budget budget;
+ const BudgetInfo& info = budget_map_[origin];
+ for (const auto& chunk : info.chunks) {
+ budget_service::BudgetChunk* budget_chunk = budget.add_budget();
+ budget_chunk->set_amount(chunk.amount);
+ budget_chunk->set_expiration(chunk.expiration.ToInternalValue());
+ }
+ budget.set_engagement_last_updated(
+ info.last_engagement_award.ToInternalValue());
+ entries->push_back(std::make_pair(origin.Serialize(), budget));
+ } else {
+ // If the origin doesn't exist in the cache, this is a remove operation.
+ keys_to_remove->push_back(origin.Serialize());
+ }
+
+ // Send the updates to the database.
+ db_->UpdateEntries(std::move(entries), std::move(keys_to_remove),
+ std::move(callback));
+}
+
+void BudgetDatabase::SyncCache(const url::Origin& origin,
+ CacheCallback callback) {
+ // If the origin isn't already cached, add it to the cache.
+ if (!IsCached(origin)) {
+ if (!db_) {
+ base::SequencedTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::BindOnce(std::move(callback), false));
+ return;
+ }
+ CacheCallback add_callback = base::BindOnce(
+ &BudgetDatabase::SyncLoadedCache, weak_ptr_factory_.GetWeakPtr(),
+ origin, std::move(callback));
+ db_->GetEntry(origin.Serialize(),
+ base::BindOnce(&BudgetDatabase::AddToCache,
+ weak_ptr_factory_.GetWeakPtr(), origin,
+ std::move(add_callback)));
+ return;
+ }
+ SyncLoadedCache(origin, std::move(callback), true /* success */);
+}
+
+void BudgetDatabase::SyncLoadedCache(const url::Origin& origin,
+ CacheCallback callback,
+ bool success) {
+ if (!success) {
+ std::move(callback).Run(false /* success */);
+ return;
+ }
+
+ // Now, cleanup any expired budget chunks for the origin.
+ bool needs_write = CleanupExpiredBudget(origin);
+
+ // Get the SES score and add engagement budget for the site.
+ AddEngagementBudget(origin);
+
+ if (needs_write)
+ WriteCachedValuesToDatabase(origin, std::move(callback));
+ else
+ std::move(callback).Run(success);
+}
+
+void BudgetDatabase::AddEngagementBudget(const url::Origin& origin) {
+ // Calculate how much budget should be awarded. The award depends on the
+ // time elapsed since the last award and the SES score.
+ // By default, give the origin kBudgetDurationInDays worth of budget, but
+ // reduce that if budget has already been given during that period.
+ base::TimeDelta elapsed = base::Days(kBudgetDurationInDays);
+ if (IsCached(origin)) {
+ elapsed = clock_->Now() - budget_map_[origin].last_engagement_award;
+ // Don't give engagement awards for periods less than an hour.
+ if (elapsed.InHours() < 1)
+ return;
+ // Cap elapsed time to the budget duration.
+ if (elapsed.InDays() > kBudgetDurationInDays)
+ elapsed = base::Days(kBudgetDurationInDays);
+ }
+
+ // Get the current SES score, and calculate the hourly budget for that score.
+ double hourly_budget = kMaximumHourlyBudget *
+ GetSiteEngagementScoreForOrigin(origin) /
+ site_engagement::SiteEngagementService::GetMaxPoints();
+
+ // Update the last_engagement_award to the current time. If the origin wasn't
+ // already in the map, this adds a new entry for it.
+ budget_map_[origin].last_engagement_award = clock_->Now();
+
+ // Add a new chunk of budget for the origin at the default expiration time.
+ base::Time expiration = clock_->Now() + base::Days(kBudgetDurationInDays);
+ budget_map_[origin].chunks.emplace_back(elapsed.InHours() * hourly_budget,
+ expiration);
+
+ // Any time we award engagement budget, which is done at most once an hour
+ // whenever any budget action is taken, record the budget.
+ double budget = GetBudget(origin);
+ UMA_HISTOGRAM_COUNTS_100("PushMessaging.BackgroundBudget", budget);
+}
+
+// Cleans up budget in the cache. Relies on the caller eventually writing the
+// cache back to the database.
+bool BudgetDatabase::CleanupExpiredBudget(const url::Origin& origin) {
+ if (!IsCached(origin))
+ return false;
+
+ base::Time now = clock_->Now();
+ BudgetChunks& chunks = budget_map_[origin].chunks;
+ auto cleanup_iter = chunks.begin();
+
+ // This relies on the list of chunks being in timestamp order.
+ while (cleanup_iter != chunks.end() && cleanup_iter->expiration <= now)
+ cleanup_iter = chunks.erase(cleanup_iter);
+
+ // If the entire budget is empty now AND there have been no engagements
+ // in the last kBudgetDurationInDays days, remove this from the cache.
+ if (chunks.empty() && budget_map_[origin].last_engagement_award <
+ clock_->Now() - base::Days(kBudgetDurationInDays)) {
+ budget_map_.erase(origin);
+ return true;
+ }
+
+ // Although some things may have expired, there are some chunks still valid.
+ // Don't write to the DB now, write either when all chunks expire or when the
+ // origin spends some budget.
+ return false;
+}
+
+double BudgetDatabase::GetSiteEngagementScoreForOrigin(
+ const url::Origin& origin) const {
+ if (profile_->IsOffTheRecord())
+ return 0;
+
+ return site_engagement::SiteEngagementService::Get(profile_)->GetScore(
+ origin.GetURL());
+}
diff --git a/chromium/chrome/browser/push_messaging/budget_database.h b/chromium/chrome/browser/push_messaging/budget_database.h
new file mode 100644
index 00000000000..0ae20374262
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/budget_database.h
@@ -0,0 +1,182 @@
+// Copyright 2016 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_PUSH_MESSAGING_BUDGET_DATABASE_H_
+#define CHROME_BROWSER_PUSH_MESSAGING_BUDGET_DATABASE_H_
+
+#include <list>
+#include <map>
+#include <memory>
+
+#include "base/callback_forward.h"
+#include "base/gtest_prod_util.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "components/leveldb_proto/public/proto_database.h"
+
+namespace base {
+class Clock;
+class Time;
+} // namespace base
+
+namespace budget_service {
+class Budget;
+}
+
+namespace url {
+class Origin;
+}
+
+class Profile;
+
+// Structure representing the budget at points in time in the future.
+struct BudgetState {
+ BudgetState();
+ BudgetState(const BudgetState& other);
+ ~BudgetState();
+
+ BudgetState& operator=(const BudgetState& other);
+
+ // Amount of budget that will be available. This should be the lower bound of
+ // the budget between this time and the previous time.
+ double budget_at = 0;
+
+ // Time at which the budget is available, in milliseconds since 00:00:00 UTC
+ // on 1 January 1970, at which the budget_at will be valid.
+ double time = 0;
+};
+
+// A class used to asynchronously read and write details of the budget
+// assigned to an origin. The class uses an underlying LevelDB.
+class BudgetDatabase {
+ public:
+ // The default amount of budget that should be spent.
+ static constexpr double kDefaultAmount = 2.0;
+
+ // Callback for getting a list of all budget chunks.
+ using GetBudgetCallback = base::OnceCallback<void(std::vector<BudgetState>)>;
+
+ // This is invoked only after the spend has been written to the database.
+ using SpendBudgetCallback = base::OnceCallback<void(bool success)>;
+
+ // The database_dir specifies the location of the budget information on disk.
+ explicit BudgetDatabase(Profile* profile);
+
+ BudgetDatabase(const BudgetDatabase&) = delete;
+ BudgetDatabase& operator=(const BudgetDatabase&) = delete;
+
+ ~BudgetDatabase();
+
+ // Get the full budget expectation for the origin. This will return a
+ // sequence of time points and the expected budget at those times.
+ void GetBudgetDetails(const url::Origin& origin, GetBudgetCallback callback);
+
+ // Spend a fixed (2.0) amount of budget for an origin. The callback indicates
+ // whether the budget could be spent for the given |origin|.
+ void SpendBudget(const url::Origin& origin,
+ SpendBudgetCallback callback,
+ double amount = kDefaultAmount);
+
+ private:
+ FRIEND_TEST_ALL_PREFIXES(BudgetDatabaseTest,
+ DefaultSiteEngagementInIncognitoProfile);
+ friend class BudgetDatabaseTest;
+
+ // Used to allow tests to change time for testing.
+ void SetClockForTesting(std::unique_ptr<base::Clock> clock);
+
+ // Holds information about individual pieces of awarded budget. There is a
+ // one-to-one mapping of these to the chunks in the underlying database.
+ struct BudgetChunk {
+ BudgetChunk(double amount, base::Time expiration)
+ : amount(amount), expiration(expiration) {}
+ BudgetChunk(const BudgetChunk&) = default;
+ BudgetChunk& operator=(const BudgetChunk&) = default;
+
+ double amount;
+ base::Time expiration;
+ };
+
+ // Data structure for caching budget information.
+ using BudgetChunks = std::list<BudgetChunk>;
+
+ // Holds information about the overall budget for a site. This includes the
+ // time the budget was last incremented, as well as a list of budget chunks
+ // which have been awarded.
+ struct BudgetInfo {
+ BudgetInfo();
+
+ BudgetInfo(const BudgetInfo&) = delete;
+ BudgetInfo& operator=(const BudgetInfo&) = delete;
+
+ BudgetInfo(const BudgetInfo&& other);
+
+ ~BudgetInfo();
+
+ base::Time last_engagement_award;
+ BudgetChunks chunks;
+ };
+
+ // Callback for writing budget values to the database.
+ using StoreBudgetCallback = base::OnceCallback<void(bool success)>;
+
+ using CacheCallback = base::OnceCallback<void(bool success)>;
+
+ void OnDatabaseInit(leveldb_proto::Enums::InitStatus status);
+
+ bool IsCached(const url::Origin& origin) const;
+
+ double GetBudget(const url::Origin& origin) const;
+
+ void AddToCache(const url::Origin& origin,
+ CacheCallback callback,
+ bool success,
+ std::unique_ptr<budget_service::Budget> budget);
+
+ void GetBudgetAfterSync(const url::Origin& origin,
+ GetBudgetCallback callback,
+ bool success);
+
+ void SpendBudgetAfterSync(const url::Origin& origin,
+ double amount,
+ SpendBudgetCallback callback,
+ bool success);
+
+ void SpendBudgetAfterWrite(SpendBudgetCallback callback, bool success);
+
+ void WriteCachedValuesToDatabase(const url::Origin& origin,
+ StoreBudgetCallback callback);
+
+ void SyncCache(const url::Origin& origin, CacheCallback callback);
+ void SyncLoadedCache(const url::Origin& origin,
+ CacheCallback callback,
+ bool success);
+
+ // Add budget based on engagement with an origin. The method queries for the
+ // engagement score of the origin, and then calculates when engagement budget
+ // was last awarded and awards a portion of the score based on that.
+ // This only writes budget to the cache.
+ void AddEngagementBudget(const url::Origin& origin);
+
+ bool CleanupExpiredBudget(const url::Origin& origin);
+
+ // Gets the current Site Engagement Score for |origin|. Will return a fixed
+ // score of zero when |profile_| is off the record.
+ double GetSiteEngagementScoreForOrigin(const url::Origin& origin) const;
+
+ raw_ptr<Profile> profile_;
+
+ // The database for storing budget information.
+ std::unique_ptr<leveldb_proto::ProtoDatabase<budget_service::Budget>> db_;
+
+ // Cached data for the origins which have been loaded.
+ std::map<url::Origin, BudgetInfo> budget_map_;
+
+ // The clock used to vend times.
+ std::unique_ptr<base::Clock> clock_;
+
+ base::WeakPtrFactory<BudgetDatabase> weak_ptr_factory_{this};
+};
+
+#endif // CHROME_BROWSER_PUSH_MESSAGING_BUDGET_DATABASE_H_
diff --git a/chromium/chrome/browser/push_messaging/budget_database_unittest.cc b/chromium/chrome/browser/push_messaging/budget_database_unittest.cc
new file mode 100644
index 00000000000..dbf29452424
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/budget_database_unittest.cc
@@ -0,0 +1,351 @@
+// Copyright 2016 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.
+
+#include "chrome/browser/push_messaging/budget_database.h"
+
+#include <math.h>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/memory/ptr_util.h"
+#include "base/run_loop.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/simple_test_clock.h"
+#include "chrome/browser/push_messaging/budget.pb.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/leveldb_proto/public/proto_database.h"
+#include "components/leveldb_proto/public/proto_database_provider.h"
+#include "components/site_engagement/content/site_engagement_score.h"
+#include "components/site_engagement/content/site_engagement_service.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+#include "url/origin.h"
+
+namespace {
+
+// These values mirror the defaults in budget_database.cc
+const double kDefaultExpirationInDays = 4;
+const double kMaxDailyBudget = 12;
+
+const double kEngagement = 25;
+
+const char kTestOrigin[] = "https://example.com";
+
+} // namespace
+
+class BudgetDatabaseTest : public ::testing::Test {
+ public:
+ BudgetDatabaseTest()
+ : success_(false),
+ db_(&profile_),
+ origin_(url::Origin::Create(GURL(kTestOrigin))) {}
+
+ void WriteBudgetComplete(base::OnceClosure run_loop_closure, bool success) {
+ success_ = success;
+ std::move(run_loop_closure).Run();
+ }
+
+ // Spend budget for the origin.
+ bool SpendBudget(double amount) {
+ base::RunLoop run_loop;
+ db_.SpendBudget(
+ origin(),
+ base::BindOnce(&BudgetDatabaseTest::WriteBudgetComplete,
+ base::Unretained(this), run_loop.QuitClosure()),
+ amount);
+ run_loop.Run();
+ return success_;
+ }
+
+ void GetBudgetDetailsComplete(base::OnceClosure run_loop_closure,
+ std::vector<BudgetState> predictions) {
+ success_ = !predictions.empty();
+ prediction_.swap(predictions);
+ std::move(run_loop_closure).Run();
+ }
+
+ // Get the full set of budget predictions for the origin.
+ void GetBudgetDetails() {
+ base::RunLoop run_loop;
+ db_.GetBudgetDetails(
+ origin(),
+ base::BindOnce(&BudgetDatabaseTest::GetBudgetDetailsComplete,
+ base::Unretained(this), run_loop.QuitClosure()));
+ run_loop.Run();
+ }
+
+ Profile* profile() { return &profile_; }
+ BudgetDatabase* database() { return &db_; }
+ const url::Origin& origin() const { return origin_; }
+
+ // Setup a test clock so that the tests can control time.
+ base::SimpleTestClock* SetClockForTesting() {
+ base::SimpleTestClock* clock = new base::SimpleTestClock();
+ db_.SetClockForTesting(base::WrapUnique(clock));
+ return clock;
+ }
+
+ void SetSiteEngagementScore(double score) {
+ site_engagement::SiteEngagementService* service =
+ site_engagement::SiteEngagementService::Get(&profile_);
+ service->ResetBaseScoreForURL(GURL(kTestOrigin), score);
+ }
+
+ protected:
+ base::HistogramTester* GetHistogramTester() { return &histogram_tester_; }
+ bool success_;
+ std::vector<BudgetState> prediction_;
+
+ private:
+ content::BrowserTaskEnvironment task_environment_;
+ TestingProfile profile_;
+ BudgetDatabase db_;
+ base::HistogramTester histogram_tester_;
+ const url::Origin origin_;
+};
+
+TEST_F(BudgetDatabaseTest, GetBudgetNoBudgetOrSES) {
+ GetBudgetDetails();
+ ASSERT_TRUE(success_);
+ ASSERT_EQ(2U, prediction_.size());
+ EXPECT_EQ(0, prediction_[0].budget_at);
+}
+
+TEST_F(BudgetDatabaseTest, AddEngagementBudgetTest) {
+ base::SimpleTestClock* clock = SetClockForTesting();
+ base::Time expiration_time =
+ clock->Now() + base::Days(kDefaultExpirationInDays);
+
+ // Set the default site engagement.
+ SetSiteEngagementScore(kEngagement);
+
+ // The budget should include kDefaultExpirationInDays days worth of
+ // engagement.
+ double daily_budget =
+ kMaxDailyBudget *
+ (kEngagement / site_engagement::SiteEngagementScore::kMaxPoints);
+ GetBudgetDetails();
+ ASSERT_TRUE(success_);
+ ASSERT_EQ(2U, prediction_.size());
+ ASSERT_DOUBLE_EQ(daily_budget * kDefaultExpirationInDays,
+ prediction_[0].budget_at);
+ ASSERT_EQ(0, prediction_[1].budget_at);
+ ASSERT_EQ(expiration_time.ToJsTime(), prediction_[1].time);
+
+ // Advance time 1 day and add more engagement budget.
+ clock->Advance(base::Days(1));
+ GetBudgetDetails();
+
+ // The budget should now have 1 full share plus 1 daily budget.
+ ASSERT_TRUE(success_);
+ ASSERT_EQ(3U, prediction_.size());
+ ASSERT_DOUBLE_EQ(daily_budget * (kDefaultExpirationInDays + 1),
+ prediction_[0].budget_at);
+ ASSERT_DOUBLE_EQ(daily_budget, prediction_[1].budget_at);
+ ASSERT_EQ(expiration_time.ToJsTime(), prediction_[1].time);
+ ASSERT_DOUBLE_EQ(0, prediction_[2].budget_at);
+ ASSERT_EQ((expiration_time + base::Days(1)).ToJsTime(), prediction_[2].time);
+
+ // Advance time by 59 minutes and check that no engagement budget is added
+ // since budget should only be added for > 1 hour increments.
+ clock->Advance(base::Minutes(59));
+ GetBudgetDetails();
+
+ // The budget should be the same as before the attempted add.
+ ASSERT_TRUE(success_);
+ ASSERT_EQ(3U, prediction_.size());
+ ASSERT_DOUBLE_EQ(daily_budget * (kDefaultExpirationInDays + 1),
+ prediction_[0].budget_at);
+}
+
+TEST_F(BudgetDatabaseTest, SpendBudgetTest) {
+ base::SimpleTestClock* clock = SetClockForTesting();
+
+ // Set the default site engagement.
+ SetSiteEngagementScore(kEngagement);
+
+ // Intialize the budget with several chunks.
+ GetBudgetDetails();
+ clock->Advance(base::Days(1));
+ GetBudgetDetails();
+ clock->Advance(base::Days(1));
+ GetBudgetDetails();
+
+ // Spend an amount of budget less than the daily budget.
+ ASSERT_TRUE(SpendBudget(1));
+ GetBudgetDetails();
+
+ // There should still be three chunks of budget of size daily_budget-1,
+ // daily_budget, and kDefaultExpirationInDays * daily_budget.
+ double daily_budget =
+ kMaxDailyBudget *
+ (kEngagement / site_engagement::SiteEngagementScore::kMaxPoints);
+ ASSERT_EQ(4U, prediction_.size());
+ ASSERT_DOUBLE_EQ((2 + kDefaultExpirationInDays) * daily_budget - 1,
+ prediction_[0].budget_at);
+ ASSERT_DOUBLE_EQ(daily_budget * 2, prediction_[1].budget_at);
+ ASSERT_DOUBLE_EQ(daily_budget, prediction_[2].budget_at);
+ ASSERT_DOUBLE_EQ(0, prediction_[3].budget_at);
+
+ // Now spend enough that it will use up the rest of the first chunk and all of
+ // the second chunk, but not all of the third chunk.
+ ASSERT_TRUE(SpendBudget((1 + kDefaultExpirationInDays) * daily_budget));
+ GetBudgetDetails();
+ ASSERT_EQ(2U, prediction_.size());
+ ASSERT_DOUBLE_EQ(daily_budget - 1, prediction_[0].budget_at);
+
+ // Validate that the code returns false if SpendBudget tries to spend more
+ // budget than the origin has.
+ EXPECT_FALSE(SpendBudget(kEngagement));
+ GetBudgetDetails();
+ ASSERT_EQ(2U, prediction_.size());
+ ASSERT_DOUBLE_EQ(daily_budget - 1, prediction_[0].budget_at);
+
+ // Advance time until the last remaining chunk should be expired, then query
+ // for the full engagement worth of budget.
+ clock->Advance(base::Days(kDefaultExpirationInDays + 1));
+ EXPECT_TRUE(SpendBudget(daily_budget * kDefaultExpirationInDays));
+}
+
+// There are times when a device's clock could move backwards in time, either
+// due to hardware issues or user actions. Test here to make sure that even if
+// time goes backwards and then forwards again, the origin isn't granted extra
+// budget.
+TEST_F(BudgetDatabaseTest, GetBudgetNegativeTime) {
+ base::SimpleTestClock* clock = SetClockForTesting();
+
+ // Set the default site engagement.
+ SetSiteEngagementScore(kEngagement);
+
+ // Initialize the budget with two chunks.
+ GetBudgetDetails();
+ clock->Advance(base::Days(1));
+ GetBudgetDetails();
+
+ // Save off the budget total.
+ ASSERT_EQ(3U, prediction_.size());
+ double budget = prediction_[0].budget_at;
+
+ // Move the clock backwards in time to before the budget awards.
+ clock->SetNow(clock->Now() - base::Days(5));
+
+ // Make sure the budget is the same.
+ GetBudgetDetails();
+ ASSERT_EQ(3U, prediction_.size());
+ ASSERT_EQ(budget, prediction_[0].budget_at);
+
+ // Now move the clock back to the original time and check that no extra budget
+ // is awarded.
+ clock->SetNow(clock->Now() + base::Days(5));
+ GetBudgetDetails();
+ ASSERT_EQ(3U, prediction_.size());
+ ASSERT_EQ(budget, prediction_[0].budget_at);
+}
+
+TEST_F(BudgetDatabaseTest, CheckBackgroundBudgetHistogram) {
+ base::SimpleTestClock* clock = SetClockForTesting();
+
+ // Set the default site engagement.
+ SetSiteEngagementScore(kEngagement);
+
+ // Initialize the budget with some interesting chunks: 30 budget (full
+ // engagement), 15 budget (half of the engagement), 0 budget (less than an
+ // hour), and then after the first two expire, another 30 budget.
+ GetBudgetDetails();
+ clock->Advance(base::Days(kDefaultExpirationInDays / 2));
+ GetBudgetDetails();
+ clock->Advance(base::Minutes(59));
+ GetBudgetDetails();
+ clock->Advance(base::Days(kDefaultExpirationInDays + 1));
+ GetBudgetDetails();
+
+ // The BackgroundBudget UMA is recorded when budget is added to the origin.
+ // This can happen a maximum of once per hour so there should be two entries.
+ std::vector<base::Bucket> buckets =
+ GetHistogramTester()->GetAllSamples("PushMessaging.BackgroundBudget");
+ ASSERT_EQ(2U, buckets.size());
+ // First bucket is for full award, which should have 2 entries.
+ double full_award = kMaxDailyBudget * kEngagement /
+ site_engagement::SiteEngagementScore::kMaxPoints *
+ kDefaultExpirationInDays;
+ EXPECT_EQ(floor(full_award), buckets[0].min);
+ EXPECT_EQ(2, buckets[0].count);
+ // Second bucket is for 1.5 * award, which should have 1 entry.
+ EXPECT_EQ(floor(full_award * 1.5), buckets[1].min);
+ EXPECT_EQ(1, buckets[1].count);
+}
+
+TEST_F(BudgetDatabaseTest, CheckEngagementHistograms) {
+ base::SimpleTestClock* clock = SetClockForTesting();
+
+ // Manipulate the engagement so that the budget is twice the cost of an
+ // action.
+ double cost = 2;
+ double engagement = 2 * cost *
+ site_engagement::SiteEngagementScore::kMaxPoints /
+ kDefaultExpirationInDays / kMaxDailyBudget;
+ SetSiteEngagementScore(engagement);
+
+ // Get the budget, which will award a chunk of budget equal to engagement.
+ GetBudgetDetails();
+
+ // Now spend the budget to trigger the UMA recording the SES score. The first
+ // call shouldn't write any UMA. The second should write a lowSES entry, and
+ // the third should write a noSES entry.
+ ASSERT_TRUE(SpendBudget(cost));
+ ASSERT_TRUE(SpendBudget(cost));
+ ASSERT_FALSE(SpendBudget(cost));
+
+ // Advance the clock by 12 days (to guarantee a full new engagement grant)
+ // then change the SES score to get a different UMA entry, then spend the
+ // budget again.
+ clock->Advance(base::Days(12));
+ GetBudgetDetails();
+ SetSiteEngagementScore(engagement * 2);
+ ASSERT_TRUE(SpendBudget(cost));
+ ASSERT_TRUE(SpendBudget(cost));
+ ASSERT_FALSE(SpendBudget(cost));
+
+ // Now check the UMA. Both UMA should have 2 buckets with 1 entry each.
+ std::vector<base::Bucket> no_budget_buckets =
+ GetHistogramTester()->GetAllSamples("PushMessaging.SESForNoBudgetOrigin");
+ ASSERT_EQ(2U, no_budget_buckets.size());
+ EXPECT_EQ(floor(engagement), no_budget_buckets[0].min);
+ EXPECT_EQ(1, no_budget_buckets[0].count);
+ EXPECT_EQ(floor(engagement * 2), no_budget_buckets[1].min);
+ EXPECT_EQ(1, no_budget_buckets[1].count);
+
+ std::vector<base::Bucket> low_budget_buckets =
+ GetHistogramTester()->GetAllSamples(
+ "PushMessaging.SESForLowBudgetOrigin");
+ ASSERT_EQ(2U, low_budget_buckets.size());
+ EXPECT_EQ(floor(engagement), low_budget_buckets[0].min);
+ EXPECT_EQ(1, low_budget_buckets[0].count);
+ EXPECT_EQ(floor(engagement * 2), low_budget_buckets[1].min);
+ EXPECT_EQ(1, low_budget_buckets[1].count);
+}
+
+TEST_F(BudgetDatabaseTest, DefaultSiteEngagementInIncognitoProfile) {
+ TestingProfile second_profile;
+ Profile* second_profile_incognito =
+ second_profile.GetPrimaryOTRProfile(/*create_if_needed=*/true);
+
+ // Create a second BudgetDatabase instance for the off-the-record version of
+ // a second profile. This will not have been influenced by the |profile_|.
+ std::unique_ptr<BudgetDatabase> second_database =
+ std::make_unique<BudgetDatabase>(second_profile_incognito);
+
+ ASSERT_FALSE(profile()->IsOffTheRecord());
+ ASSERT_FALSE(second_profile.IsOffTheRecord());
+ ASSERT_TRUE(second_profile_incognito->IsOffTheRecord());
+
+ // The Site Engagement Score considered by an Incognito profile must be equal
+ // to the score considered in a regular profile visting a page for the first
+ // time. This may grant a small amount of budget, but does mean that Incognito
+ // mode cannot be detected through the Budget API.
+ EXPECT_EQ(database()->GetSiteEngagementScoreForOrigin(origin()),
+ second_database->GetSiteEngagementScoreForOrigin(origin()));
+}
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.cc b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.cc
new file mode 100644
index 00000000000..a09e96fba38
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.cc
@@ -0,0 +1,322 @@
+// Copyright 2014 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.
+
+#include "chrome/browser/push_messaging/push_messaging_app_identifier.h"
+
+#include <string.h>
+
+#include "base/check_op.h"
+#include "base/guid.h"
+#include "base/notreached.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_split.h"
+#include "base/strings/string_util.h"
+#include "base/values.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/common/pref_names.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/prefs/pref_service.h"
+#include "components/prefs/scoped_user_pref_update.h"
+
+constexpr char kPushMessagingAppIdentifierPrefix[] = "wp:";
+constexpr char kInstanceIDGuidSuffix[] = "-V2";
+
+namespace {
+
+// sizeof is strlen + 1 since it's null-terminated.
+constexpr size_t kPrefixLength = sizeof(kPushMessagingAppIdentifierPrefix) - 1;
+constexpr size_t kGuidSuffixLength = sizeof(kInstanceIDGuidSuffix) - 1;
+
+// Ok to use '#' as separator since only the origin of the url is used.
+constexpr char kPrefValueSeparator = '#';
+constexpr size_t kGuidLength = 36; // "%08X-%04X-%04X-%04X-%012llX"
+
+std::string FromTimeToString(base::Time time) {
+ DCHECK(!time.is_null());
+ return base::NumberToString(time.ToDeltaSinceWindowsEpoch().InMilliseconds());
+}
+
+bool FromStringToTime(const std::string& time_string,
+ absl::optional<base::Time>* time) {
+ DCHECK(!time_string.empty());
+ int64_t milliseconds;
+ if (base::StringToInt64(time_string, &milliseconds) && milliseconds > 0) {
+ *time = absl::make_optional(base::Time::FromDeltaSinceWindowsEpoch(
+ base::Milliseconds(milliseconds)));
+ return true;
+ }
+ return false;
+}
+
+std::string MakePrefValue(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const absl::optional<base::Time>& expiration_time = absl::nullopt) {
+ std::string result = origin.spec() + kPrefValueSeparator +
+ base::NumberToString(service_worker_registration_id);
+ if (expiration_time)
+ result += kPrefValueSeparator + FromTimeToString(*expiration_time);
+ return result;
+}
+
+bool DisassemblePrefValue(const std::string& pref_value,
+ GURL* origin,
+ int64_t* service_worker_registration_id,
+ absl::optional<base::Time>* expiration_time) {
+ std::vector<std::string> parts =
+ base::SplitString(pref_value, std::string(1, kPrefValueSeparator),
+ base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+
+ if (parts.size() < 2 || parts.size() > 3)
+ return false;
+
+ if (!base::StringToInt64(parts[1], service_worker_registration_id))
+ return false;
+
+ *origin = GURL(parts[0]);
+ if (!origin->is_valid())
+ return false;
+
+ if (parts.size() == 3)
+ return FromStringToTime(parts[2], expiration_time);
+
+ return true;
+}
+
+} // namespace
+
+// static
+void PushMessagingAppIdentifier::RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) {
+ // TODO(johnme): If push becomes enabled in incognito, be careful that this
+ // pref is read from the right profile, as prefs defined in a regular profile
+ // are visible in the corresponding incognito profile unless overridden.
+ // TODO(johnme): Make sure this pref doesn't get out of sync after crashes.
+ registry->RegisterDictionaryPref(prefs::kPushMessagingAppIdentifierMap);
+}
+
+// static
+bool PushMessagingAppIdentifier::UseInstanceID(const std::string& app_id) {
+ return base::EndsWith(app_id, kInstanceIDGuidSuffix,
+ base::CompareCase::SENSITIVE);
+}
+
+// static
+PushMessagingAppIdentifier PushMessagingAppIdentifier::Generate(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const absl::optional<base::Time>& expiration_time) {
+ // All new push subscriptions use Instance ID tokens.
+ return GenerateInternal(origin, service_worker_registration_id,
+ true /* use_instance_id */, expiration_time);
+}
+
+// static
+PushMessagingAppIdentifier PushMessagingAppIdentifier::LegacyGenerateForTesting(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const absl::optional<base::Time>& expiration_time) {
+ return GenerateInternal(origin, service_worker_registration_id,
+ false /* use_instance_id */, expiration_time);
+}
+
+// static
+PushMessagingAppIdentifier PushMessagingAppIdentifier::GenerateInternal(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ bool use_instance_id,
+ const absl::optional<base::Time>& expiration_time) {
+ // Use uppercase GUID for consistency with GUIDs Push has already sent to GCM.
+ // Also allows detecting case mangling; see code commented "crbug.com/461867".
+ std::string guid = base::ToUpperASCII(base::GenerateGUID());
+ if (use_instance_id) {
+ guid.replace(guid.size() - kGuidSuffixLength, kGuidSuffixLength,
+ kInstanceIDGuidSuffix);
+ }
+ CHECK(!guid.empty());
+ std::string app_id = kPushMessagingAppIdentifierPrefix + origin.spec() +
+ kPrefValueSeparator + guid;
+
+ PushMessagingAppIdentifier app_identifier(
+ app_id, origin, service_worker_registration_id, expiration_time);
+ app_identifier.DCheckValid();
+ return app_identifier;
+}
+
+// static
+PushMessagingAppIdentifier PushMessagingAppIdentifier::FindByAppId(
+ Profile* profile, const std::string& app_id) {
+ if (!base::StartsWith(app_id, kPushMessagingAppIdentifierPrefix,
+ base::CompareCase::INSENSITIVE_ASCII)) {
+ return PushMessagingAppIdentifier();
+ }
+
+ // Since we now know this is a Push Messaging app_id, check the case hasn't
+ // been mangled (crbug.com/461867).
+ DCHECK_EQ(kPushMessagingAppIdentifierPrefix, app_id.substr(0, kPrefixLength));
+ DCHECK_GE(app_id.size(), kPrefixLength + kGuidLength);
+ DCHECK_EQ(app_id.substr(app_id.size() - kGuidLength),
+ base::ToUpperASCII(app_id.substr(app_id.size() - kGuidLength)));
+
+ const base::Value* map =
+ profile->GetPrefs()->GetDictionary(prefs::kPushMessagingAppIdentifierMap);
+
+ const std::string* map_value = map->FindStringKey(app_id);
+
+ if (!map_value || map_value->empty())
+ return PushMessagingAppIdentifier();
+
+ GURL origin;
+ int64_t service_worker_registration_id;
+ absl::optional<base::Time> expiration_time;
+ // Try disassemble the pref value, return an invalid app identifier if the
+ // pref value is corrupted
+ if (!DisassemblePrefValue(*map_value, &origin,
+ &service_worker_registration_id,
+ &expiration_time)) {
+ NOTREACHED();
+ return PushMessagingAppIdentifier();
+ }
+
+ PushMessagingAppIdentifier app_identifier(
+ app_id, origin, service_worker_registration_id, expiration_time);
+ app_identifier.DCheckValid();
+ return app_identifier;
+}
+
+// static
+PushMessagingAppIdentifier PushMessagingAppIdentifier::FindByServiceWorker(
+ Profile* profile,
+ const GURL& origin,
+ int64_t service_worker_registration_id) {
+ const std::string base_pref_value =
+ MakePrefValue(origin, service_worker_registration_id);
+
+ const base::Value* map =
+ profile->GetPrefs()->GetDictionary(prefs::kPushMessagingAppIdentifierMap);
+ for (auto entry : map->DictItems()) {
+ if (entry.second.is_string() &&
+ base::StartsWith(entry.second.GetString(), base_pref_value,
+ base::CompareCase::SENSITIVE)) {
+ return FindByAppId(profile, entry.first);
+ }
+ }
+ return PushMessagingAppIdentifier();
+}
+
+// static
+std::vector<PushMessagingAppIdentifier> PushMessagingAppIdentifier::GetAll(
+ Profile* profile) {
+ std::vector<PushMessagingAppIdentifier> result;
+
+ const base::Value* map =
+ profile->GetPrefs()->GetDictionary(prefs::kPushMessagingAppIdentifierMap);
+ for (auto entry : map->DictItems()) {
+ result.push_back(FindByAppId(profile, entry.first));
+ }
+
+ return result;
+}
+
+// static
+void PushMessagingAppIdentifier::DeleteAllFromPrefs(Profile* profile) {
+ DictionaryPrefUpdate update(profile->GetPrefs(),
+ prefs::kPushMessagingAppIdentifierMap);
+ base::Value* map = update.Get();
+ map->DictClear();
+}
+
+// static
+size_t PushMessagingAppIdentifier::GetCount(Profile* profile) {
+ return profile->GetPrefs()
+ ->GetDictionary(prefs::kPushMessagingAppIdentifierMap)
+ ->DictSize();
+}
+
+PushMessagingAppIdentifier::PushMessagingAppIdentifier(
+ const PushMessagingAppIdentifier& other) = default;
+
+PushMessagingAppIdentifier::PushMessagingAppIdentifier()
+ : origin_(GURL::EmptyGURL()), service_worker_registration_id_(-1) {}
+
+PushMessagingAppIdentifier::PushMessagingAppIdentifier(
+ const std::string& app_id,
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const absl::optional<base::Time>& expiration_time)
+ : app_id_(app_id),
+ origin_(origin),
+ service_worker_registration_id_(service_worker_registration_id),
+ expiration_time_(expiration_time) {}
+
+PushMessagingAppIdentifier::~PushMessagingAppIdentifier() {}
+
+bool PushMessagingAppIdentifier::IsExpired() const {
+ return (expiration_time_) ? *expiration_time_ < base::Time::Now() : false;
+}
+
+void PushMessagingAppIdentifier::PersistToPrefs(Profile* profile) const {
+ DCheckValid();
+
+ DictionaryPrefUpdate update(profile->GetPrefs(),
+ prefs::kPushMessagingAppIdentifierMap);
+ base::Value* map = update.Get();
+
+ // Delete any stale entry with the same origin and Service Worker
+ // registration id (hence we ensure there is a 1:1 not 1:many mapping).
+ PushMessagingAppIdentifier old =
+ FindByServiceWorker(profile, origin_, service_worker_registration_id_);
+ if (!old.is_null())
+ map->RemoveKey(old.app_id_);
+
+ map->SetKey(app_id_,
+ base::Value(MakePrefValue(
+ origin_, service_worker_registration_id_, expiration_time_)));
+}
+
+void PushMessagingAppIdentifier::DeleteFromPrefs(Profile* profile) const {
+ DCheckValid();
+
+ DictionaryPrefUpdate update(profile->GetPrefs(),
+ prefs::kPushMessagingAppIdentifierMap);
+ base::Value* map = update.Get();
+ map->RemoveKey(app_id_);
+}
+
+void PushMessagingAppIdentifier::DCheckValid() const {
+#if DCHECK_IS_ON()
+ DCHECK_GE(service_worker_registration_id_, 0);
+
+ DCHECK(origin_.is_valid());
+ DCHECK_EQ(origin_.DeprecatedGetOriginAsURL(), origin_);
+
+ // "wp:"
+ DCHECK_EQ(kPushMessagingAppIdentifierPrefix,
+ app_id_.substr(0, kPrefixLength));
+
+ // Optional (origin.spec() + '#')
+ if (app_id_.size() != kPrefixLength + kGuidLength) {
+ constexpr size_t suffix_length = 1 /* kPrefValueSeparator */ + kGuidLength;
+ DCHECK_GT(app_id_.size(), kPrefixLength + suffix_length);
+ DCHECK_EQ(origin_, GURL(app_id_.substr(
+ kPrefixLength,
+ app_id_.size() - kPrefixLength - suffix_length)));
+ DCHECK_EQ(std::string(1, kPrefValueSeparator),
+ app_id_.substr(app_id_.size() - suffix_length, 1));
+ }
+
+ // GUID. In order to distinguish them, an app_id created for an InstanceID
+ // based subscription has the last few characters of the GUID overwritten with
+ // kInstanceIDGuidSuffix (which contains non-hex characters invalid in GUIDs).
+ std::string guid = app_id_.substr(app_id_.size() - kGuidLength);
+ if (UseInstanceID(app_id_)) {
+ DCHECK(!base::IsValidGUID(guid));
+
+ // Replace suffix with valid hex so we can validate the rest of the string.
+ guid = guid.replace(guid.size() - kGuidSuffixLength, kGuidSuffixLength,
+ kGuidSuffixLength, 'C');
+ }
+ DCHECK(base::IsValidGUID(guid));
+#endif // DCHECK_IS_ON()
+}
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.h b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.h
new file mode 100644
index 00000000000..631976003fc
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.h
@@ -0,0 +1,152 @@
+// Copyright 2014 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_PUSH_MESSAGING_PUSH_MESSAGING_APP_IDENTIFIER_H_
+#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_APP_IDENTIFIER_H_
+
+#include <stddef.h>
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+#include "base/check.h"
+#include "base/gtest_prod_util.h"
+#include "base/time/time.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "url/gurl.h"
+
+class Profile;
+
+namespace user_prefs {
+class PrefRegistrySyncable;
+}
+
+// The prefix used for all push messaging application ids.
+extern const char kPushMessagingAppIdentifierPrefix[];
+
+// Type used to identify a Service Worker registration from a Push API
+// perspective. These can be persisted to prefs, in a 1:1 mapping between
+// app_id (which includes origin) and service_worker_registration_id.
+// Legacy mapped values saved by old versions of Chrome are also supported;
+// these don't contain the origin in the app_id, so instead they map from
+// app_id to pair<origin, service_worker_registration_id>.
+class PushMessagingAppIdentifier {
+ public:
+ // Register profile-specific prefs.
+ static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);
+
+ // Returns whether the modern InstanceID API should be used with this app_id
+ // (rather than legacy GCM registration).
+ static bool UseInstanceID(const std::string& app_id);
+
+ // Generates a new app identifier, with partially random app_id.
+ static PushMessagingAppIdentifier Generate(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const absl::optional<base::Time>& expiration_time = absl::nullopt);
+
+ // Looks up an app identifier by app_id. If not found, is_null() will be true.
+ static PushMessagingAppIdentifier FindByAppId(Profile* profile,
+ const std::string& app_id);
+
+ // Looks up an app identifier by origin & service worker registration id.
+ // If not found, is_null() will be true.
+ static PushMessagingAppIdentifier FindByServiceWorker(
+ Profile* profile,
+ const GURL& origin,
+ int64_t service_worker_registration_id);
+
+ // Returns all the PushMessagingAppIdentifiers currently registered for the
+ // given |profile|.
+ static std::vector<PushMessagingAppIdentifier> GetAll(Profile* profile);
+
+ // Deletes all PushMessagingAppIdentifiers currently registered for the given
+ // |profile|.
+ static void DeleteAllFromPrefs(Profile* profile);
+
+ // Returns the number of PushMessagingAppIdentifiers currently registered for
+ // the given |profile|.
+ static size_t GetCount(Profile* profile);
+
+ ~PushMessagingAppIdentifier();
+
+ // Persist this app identifier to prefs.
+ void PersistToPrefs(Profile* profile) const;
+
+ // Delete this app identifier from prefs.
+ void DeleteFromPrefs(Profile* profile) const;
+
+ // Returns true if this identifier does not represent an app (i.e. this was
+ // returned by a failed Find call).
+ bool is_null() const { return service_worker_registration_id_ < 0; }
+
+ // String that should be passed to push services like GCM to identify a
+ // particular Service Worker (so we can route incoming messages). Example:
+ // wp:https://foo.example.com:8443/#9CC55CCE-B8F9-4092-A364-3B0F73A3AB5F
+ // Legacy app_ids have no origin, e.g. wp:9CC55CCE-B8F9-4092-A364-3B0F73A3AB5F
+ const std::string& app_id() const {
+ DCHECK(!is_null());
+ return app_id_;
+ }
+
+ const GURL& origin() const {
+ DCHECK(!is_null());
+ return origin_;
+ }
+
+ int64_t service_worker_registration_id() const {
+ DCHECK(!is_null());
+ return service_worker_registration_id_;
+ }
+
+ void set_expiration_time(const absl::optional<base::Time>& expiration_time) {
+ expiration_time_ = expiration_time;
+ }
+
+ bool IsExpired() const;
+
+ absl::optional<base::Time> expiration_time() const {
+ DCHECK(!is_null());
+ return expiration_time_;
+ }
+
+ // Copy constructor
+ PushMessagingAppIdentifier(const PushMessagingAppIdentifier& other);
+
+ private:
+ friend class PushMessagingAppIdentifierTest;
+ friend class PushMessagingBrowserTestBase;
+ FRIEND_TEST_ALL_PREFIXES(PushMessagingAppIdentifierTest, FindLegacy);
+
+ // Generates a new app identifier for legacy GCM (not modern InstanceID).
+ static PushMessagingAppIdentifier LegacyGenerateForTesting(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const absl::optional<base::Time>& expiration_time = absl::nullopt);
+
+ static PushMessagingAppIdentifier GenerateInternal(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ bool use_instance_id,
+ const absl::optional<base::Time>& expiration_time = absl::nullopt);
+
+ // Constructs an invalid app identifier.
+ PushMessagingAppIdentifier();
+ // Constructs a valid app identifier.
+ PushMessagingAppIdentifier(
+ const std::string& app_id,
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const absl::optional<base::Time>& expiration_time = absl::nullopt);
+
+ // Validates that all the fields contain valid values.
+ void DCheckValid() const;
+
+ std::string app_id_;
+ GURL origin_;
+ int64_t service_worker_registration_id_;
+ absl::optional<base::Time> expiration_time_;
+};
+
+#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_APP_IDENTIFIER_H_
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_app_identifier_unittest.cc b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier_unittest.cc
new file mode 100644
index 00000000000..5b7c649c2e4
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier_unittest.cc
@@ -0,0 +1,310 @@
+// Copyright 2014 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.
+
+#include "chrome/browser/push_messaging/push_messaging_app_identifier.h"
+
+#include <stdint.h>
+
+#include "base/time/time.h"
+#include "chrome/test/base/testing_profile.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+void ExpectAppIdentifiersEqual(const PushMessagingAppIdentifier& a,
+ const PushMessagingAppIdentifier& b) {
+ EXPECT_EQ(a.app_id(), b.app_id());
+ EXPECT_EQ(a.origin(), b.origin());
+ EXPECT_EQ(a.service_worker_registration_id(),
+ b.service_worker_registration_id());
+ EXPECT_EQ(a.expiration_time(), b.expiration_time());
+}
+
+base::Time kExpirationTime =
+ base::Time::FromDeltaSinceWindowsEpoch(base::Seconds(1));
+
+} // namespace
+
+class PushMessagingAppIdentifierTest : public testing::Test {
+ protected:
+ PushMessagingAppIdentifier GenerateId(
+ const GURL& origin,
+ int64_t service_worker_registration_id) {
+ // To bypass DCHECK in PushMessagingAppIdentifier::Generate, we just use it
+ // to generate app_id, and then use private constructor.
+ std::string app_id = PushMessagingAppIdentifier::Generate(
+ GURL("https://www.example.com/"), 1)
+ .app_id();
+ return PushMessagingAppIdentifier(app_id, origin,
+ service_worker_registration_id);
+ }
+
+ void SetUp() override {
+ original_ = PushMessagingAppIdentifier::Generate(
+ GURL("https://www.example.com/"), 1);
+ same_origin_and_sw_ = PushMessagingAppIdentifier::Generate(
+ GURL("https://www.example.com"), 1);
+ different_origin_ = PushMessagingAppIdentifier::Generate(
+ GURL("https://foobar.example.com/"), 1);
+ different_sw_ = PushMessagingAppIdentifier::Generate(
+ GURL("https://www.example.com/"), 42);
+ with_et_ = PushMessagingAppIdentifier::Generate(
+ GURL("https://www.example.com/"), 1, kExpirationTime);
+ different_et_ = PushMessagingAppIdentifier::Generate(
+ GURL("https://www.example.com/"), 1,
+ kExpirationTime + base::Seconds(100));
+ }
+
+ Profile* profile() { return &profile_; }
+
+ PushMessagingAppIdentifier original_;
+ PushMessagingAppIdentifier same_origin_and_sw_;
+ PushMessagingAppIdentifier different_origin_;
+ PushMessagingAppIdentifier different_sw_;
+ PushMessagingAppIdentifier different_et_;
+ PushMessagingAppIdentifier with_et_;
+
+ private:
+ content::BrowserTaskEnvironment task_environment_;
+ TestingProfile profile_;
+};
+
+TEST_F(PushMessagingAppIdentifierTest, ConstructorValidity) {
+ // The following two are valid:
+ EXPECT_FALSE(GenerateId(GURL("https://www.example.com/"), 1).is_null());
+ EXPECT_FALSE(GenerateId(GURL("https://www.example.com"), 1).is_null());
+ // The following four are invalid and will DCHECK in Generate:
+ EXPECT_FALSE(GenerateId(GURL(""), 1).is_null());
+ EXPECT_FALSE(GenerateId(GURL("foo"), 1).is_null());
+ EXPECT_FALSE(GenerateId(GURL("https://www.example.com/foo"), 1).is_null());
+ EXPECT_FALSE(GenerateId(GURL("https://www.example.com/#foo"), 1).is_null());
+ // The following one is invalid and will DCHECK in Generate and be null:
+ EXPECT_TRUE(GenerateId(GURL("https://www.example.com/"), -1).is_null());
+}
+
+TEST_F(PushMessagingAppIdentifierTest, UniqueGuids) {
+ EXPECT_NE(
+ PushMessagingAppIdentifier::Generate(GURL("https://www.example.com/"), 1)
+ .app_id(),
+ PushMessagingAppIdentifier::Generate(GURL("https://www.example.com/"), 1)
+ .app_id());
+}
+
+TEST_F(PushMessagingAppIdentifierTest, FindInvalidAppId) {
+ // These calls to FindByAppId should not DCHECK.
+ EXPECT_TRUE(PushMessagingAppIdentifier::FindByAppId(profile(), "").is_null());
+ EXPECT_TRUE(PushMessagingAppIdentifier::FindByAppId(
+ profile(), "amhfneadkjmnlefnpidcijoldiibcdnd")
+ .is_null());
+}
+
+TEST_F(PushMessagingAppIdentifierTest, PersistAndFind) {
+ ASSERT_TRUE(
+ PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id())
+ .is_null());
+
+ const auto identifier = PushMessagingAppIdentifier::FindByServiceWorker(
+ profile(), original_.origin(),
+ original_.service_worker_registration_id());
+
+ ASSERT_TRUE(identifier.is_null());
+
+ // Test basic PersistToPrefs round trips.
+ original_.PersistToPrefs(profile());
+ {
+ PushMessagingAppIdentifier found_by_app_id =
+ PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id());
+ EXPECT_FALSE(found_by_app_id.is_null());
+ ExpectAppIdentifiersEqual(original_, found_by_app_id);
+ }
+ {
+ PushMessagingAppIdentifier found_by_origin_and_swr_id =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile(), original_.origin(),
+ original_.service_worker_registration_id());
+ EXPECT_FALSE(found_by_origin_and_swr_id.is_null());
+ ExpectAppIdentifiersEqual(original_, found_by_origin_and_swr_id);
+ }
+}
+
+TEST_F(PushMessagingAppIdentifierTest, FindLegacy) {
+ const std::string legacy_app_id("wp:9CC55CCE-B8F9-4092-A364-3B0F73A3AB5F");
+ ASSERT_TRUE(PushMessagingAppIdentifier::FindByAppId(profile(), legacy_app_id)
+ .is_null());
+
+ const auto identifier = PushMessagingAppIdentifier::FindByServiceWorker(
+ profile(), original_.origin(),
+ original_.service_worker_registration_id());
+
+ ASSERT_TRUE(identifier.is_null());
+
+ // Create a legacy preferences entry (the test happens to use PersistToPrefs
+ // since that currently works, but it's ok to change the behavior of
+ // PersistToPrefs; if so, this test can just do a raw DictionaryPrefUpdate).
+ original_.app_id_ = legacy_app_id;
+ original_.PersistToPrefs(profile());
+
+ // Test that legacy entries can be read back from prefs.
+ {
+ PushMessagingAppIdentifier found_by_app_id =
+ PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id());
+ EXPECT_FALSE(found_by_app_id.is_null());
+ ExpectAppIdentifiersEqual(original_, found_by_app_id);
+ }
+ {
+ PushMessagingAppIdentifier found_by_origin_and_swr_id =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile(), original_.origin(),
+ original_.service_worker_registration_id());
+ EXPECT_FALSE(found_by_origin_and_swr_id.is_null());
+ ExpectAppIdentifiersEqual(original_, found_by_origin_and_swr_id);
+ }
+}
+
+TEST_F(PushMessagingAppIdentifierTest, PersistOverwritesSameOriginAndSW) {
+ original_.PersistToPrefs(profile());
+
+ // Test that PersistToPrefs overwrites when same origin and Service Worker.
+ ASSERT_NE(original_.app_id(), same_origin_and_sw_.app_id());
+ ASSERT_EQ(original_.origin(), same_origin_and_sw_.origin());
+ ASSERT_EQ(original_.service_worker_registration_id(),
+ same_origin_and_sw_.service_worker_registration_id());
+ same_origin_and_sw_.PersistToPrefs(profile());
+ {
+ PushMessagingAppIdentifier found_by_original_app_id =
+ PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id());
+ EXPECT_TRUE(found_by_original_app_id.is_null());
+ }
+ {
+ PushMessagingAppIdentifier found_by_soas_app_id =
+ PushMessagingAppIdentifier::FindByAppId(profile(),
+ same_origin_and_sw_.app_id());
+ EXPECT_FALSE(found_by_soas_app_id.is_null());
+ ExpectAppIdentifiersEqual(same_origin_and_sw_, found_by_soas_app_id);
+ }
+ {
+ PushMessagingAppIdentifier found_by_original_origin_and_swr_id =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile(), original_.origin(),
+ original_.service_worker_registration_id());
+ EXPECT_FALSE(found_by_original_origin_and_swr_id.is_null());
+ ExpectAppIdentifiersEqual(same_origin_and_sw_,
+ found_by_original_origin_and_swr_id);
+ }
+}
+
+TEST_F(PushMessagingAppIdentifierTest, PersistDoesNotOverwriteDifferent) {
+ original_.PersistToPrefs(profile());
+
+ // Test that PersistToPrefs doesn't overwrite when different origin or SW.
+ ASSERT_NE(original_.app_id(), different_origin_.app_id());
+ ASSERT_NE(original_.app_id(), different_sw_.app_id());
+ different_origin_.PersistToPrefs(profile());
+ different_sw_.PersistToPrefs(profile());
+ {
+ PushMessagingAppIdentifier found_by_original_app_id =
+ PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id());
+ EXPECT_FALSE(found_by_original_app_id.is_null());
+ ExpectAppIdentifiersEqual(original_, found_by_original_app_id);
+ }
+ {
+ PushMessagingAppIdentifier found_by_original_origin_and_swr_id =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile(), original_.origin(),
+ original_.service_worker_registration_id());
+ EXPECT_FALSE(found_by_original_origin_and_swr_id.is_null());
+ ExpectAppIdentifiersEqual(original_, found_by_original_origin_and_swr_id);
+ }
+}
+
+TEST_F(PushMessagingAppIdentifierTest, DeleteFromPrefs) {
+ original_.PersistToPrefs(profile());
+ different_origin_.PersistToPrefs(profile());
+ different_sw_.PersistToPrefs(profile());
+
+ // Test DeleteFromPrefs. Deleted app identifier should be deleted.
+ original_.DeleteFromPrefs(profile());
+ {
+ PushMessagingAppIdentifier found_by_original_app_id =
+ PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id());
+ EXPECT_TRUE(found_by_original_app_id.is_null());
+ }
+ {
+ PushMessagingAppIdentifier found_by_original_origin_and_swr_id =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile(), original_.origin(),
+ original_.service_worker_registration_id());
+ EXPECT_TRUE(found_by_original_origin_and_swr_id.is_null());
+ }
+}
+
+TEST_F(PushMessagingAppIdentifierTest, GetAll) {
+ original_.PersistToPrefs(profile());
+ different_origin_.PersistToPrefs(profile());
+ different_sw_.PersistToPrefs(profile());
+
+ original_.DeleteFromPrefs(profile());
+
+ // Test GetAll. Non-deleted app identifiers should all be listed.
+ std::vector<PushMessagingAppIdentifier> all_app_identifiers =
+ PushMessagingAppIdentifier::GetAll(profile());
+ EXPECT_EQ(2u, all_app_identifiers.size());
+ // Order is unspecified.
+ bool contained_different_origin = false;
+ bool contained_different_sw = false;
+ for (const PushMessagingAppIdentifier& app_identifier : all_app_identifiers) {
+ if (app_identifier.app_id() == different_origin_.app_id()) {
+ ExpectAppIdentifiersEqual(different_origin_, app_identifier);
+ contained_different_origin = true;
+ } else {
+ ExpectAppIdentifiersEqual(different_sw_, app_identifier);
+ contained_different_sw = true;
+ }
+ }
+ EXPECT_TRUE(contained_different_origin);
+ EXPECT_TRUE(contained_different_sw);
+}
+
+TEST_F(PushMessagingAppIdentifierTest, PersistWithExpirationTime) {
+ ASSERT_TRUE(with_et_.expiration_time());
+ ASSERT_TRUE(different_et_.expiration_time());
+ ASSERT_EQ(with_et_.origin(), different_et_.origin());
+ ASSERT_EQ(with_et_.service_worker_registration_id(),
+ different_et_.service_worker_registration_id());
+ ASSERT_FALSE(kExpirationTime.is_null());
+
+ different_et_.PersistToPrefs(profile());
+
+ // Test PersistToPrefs and FindByAppId, whether expiration time is saved
+ // properly
+ std::vector<PushMessagingAppIdentifier> all_app_identifiers =
+ PushMessagingAppIdentifier::GetAll(profile());
+ EXPECT_EQ(1u, all_app_identifiers.size());
+ {
+ PushMessagingAppIdentifier found_by_app_id =
+ PushMessagingAppIdentifier::FindByAppId(profile(),
+ different_et_.app_id());
+ // Check whether expiration time was saved
+ ExpectAppIdentifiersEqual(found_by_app_id, different_et_);
+ }
+ with_et_.PersistToPrefs(profile());
+ {
+ all_app_identifiers = PushMessagingAppIdentifier::GetAll(profile());
+ EXPECT_EQ(1u, all_app_identifiers.size());
+ }
+ {
+ PushMessagingAppIdentifier found_by_with_et_app_id =
+ PushMessagingAppIdentifier::FindByAppId(profile(), with_et_.app_id());
+ EXPECT_FALSE(found_by_with_et_app_id.is_null());
+ EXPECT_EQ(found_by_with_et_app_id.expiration_time(), kExpirationTime);
+ ExpectAppIdentifiersEqual(found_by_with_et_app_id, with_et_);
+ }
+ {
+ PushMessagingAppIdentifier found_by_different_et_app_id =
+ PushMessagingAppIdentifier::FindByAppId(profile(),
+ different_et_.app_id());
+ EXPECT_TRUE(found_by_different_et_app_id.is_null());
+ }
+}
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_browsertest.cc b/chromium/chrome/browser/push_messaging/push_messaging_browsertest.cc
new file mode 100644
index 00000000000..b94212aad9f
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_browsertest.cc
@@ -0,0 +1,3227 @@
+// Copyright 2014 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.
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include "base/barrier_closure.h"
+#include "base/base64url.h"
+#include "base/bind.h"
+#include "base/command_line.h"
+#include "base/memory/raw_ptr.h"
+#include "base/run_loop.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/test/bind.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
+#include "build/build_config.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/browsing_data/chrome_browsing_data_remover_constants.h"
+#include "chrome/browser/chrome_notification_types.h"
+#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
+#include "chrome/browser/gcm/gcm_profile_service_factory.h"
+#include "chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h"
+#include "chrome/browser/notifications/notification_display_service_tester.h"
+#include "chrome/browser/notifications/notification_handler.h"
+#include "chrome/browser/permissions/crowd_deny_fake_safe_browsing_database_manager.h"
+#include "chrome/browser/permissions/crowd_deny_preload_data.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/push_messaging/push_messaging_app_identifier.h"
+#include "chrome/browser/push_messaging/push_messaging_constants.h"
+#include "chrome/browser/push_messaging/push_messaging_features.h"
+#include "chrome/browser/push_messaging/push_messaging_service_factory.h"
+#include "chrome/browser/push_messaging/push_messaging_service_impl.h"
+#include "chrome/browser/safe_browsing/test_safe_browsing_service.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/common/buildflags.h"
+#include "chrome/common/chrome_features.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/browsing_data/content/browsing_data_helper.h"
+#include "components/content_settings/core/browser/host_content_settings_map.h"
+#include "components/content_settings/core/common/content_settings.h"
+#include "components/content_settings/core/common/content_settings_types.h"
+#include "components/gcm_driver/common/gcm_message.h"
+#include "components/gcm_driver/fake_gcm_profile_service.h"
+#include "components/gcm_driver/gcm_client.h"
+#include "components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.h"
+#include "components/gcm_driver/instance_id/instance_id_driver.h"
+#include "components/gcm_driver/instance_id/instance_id_profile_service.h"
+#include "components/keep_alive_registry/keep_alive_registry.h"
+#include "components/keep_alive_registry/keep_alive_types.h"
+#include "components/network_session_configurator/common/network_switches.h"
+#include "components/permissions/permission_request_manager.h"
+#include "components/site_engagement/content/site_engagement_score.h"
+#include "components/site_engagement/content/site_engagement_service.h"
+#include "content/public/browser/browsing_data_remover.h"
+#include "content/public/browser/notification_service.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/common/content_features.h"
+#include "content/public/common/content_switches.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "content/public/test/browsing_data_remover_test_util.h"
+#include "content/public/test/prerender_test_util.h"
+#include "content/public/test/test_utils.h"
+#include "net/dns/mock_host_resolver.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "third_party/blink/public/mojom/push_messaging/push_messaging.mojom.h"
+#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h"
+#include "ui/base/window_open_disposition.h"
+#include "ui/message_center/public/cpp/notification.h"
+
+#if BUILDFLAG(ENABLE_BACKGROUND_MODE)
+#include "chrome/browser/background/background_mode_manager.h"
+#endif
+
+namespace {
+
+const char kManifestSenderId[] = "1234567890";
+const int32_t kApplicationServerKeyLength = 65;
+
+enum class PushSubscriptionKeyFormat { kOmitKey, kBinary, kBase64UrlEncoded };
+
+// NIST P-256 public key made available to tests. Must be an uncompressed
+// point in accordance with SEC1 2.3.3.
+const uint8_t kApplicationServerKey[kApplicationServerKeyLength] = {
+ 0x04, 0x55, 0x52, 0x6A, 0xA5, 0x6E, 0x8E, 0xAA, 0x47, 0x97, 0x36,
+ 0x10, 0xC1, 0x66, 0x3C, 0x1E, 0x65, 0xBF, 0xA1, 0x7B, 0xEE, 0x48,
+ 0xC9, 0xC6, 0xBB, 0xBF, 0x02, 0x18, 0x53, 0x72, 0x1D, 0x0C, 0x7B,
+ 0xA9, 0xE3, 0x11, 0xB7, 0x03, 0x52, 0x21, 0xD3, 0x71, 0x90, 0x13,
+ 0xA8, 0xC1, 0xCF, 0xED, 0x20, 0xF7, 0x1F, 0xD1, 0x7F, 0xF2, 0x76,
+ 0xB6, 0x01, 0x20, 0xD8, 0x35, 0xA5, 0xD9, 0x3C, 0x43, 0xFD};
+
+// URL-safe base64 encoded version of the |kApplicationServerKey|.
+const char kEncodedApplicationServerKey[] =
+ "BFVSaqVujqpHlzYQwWY8HmW_oXvuSMnGu78CGFNyHQx7qeMRtwNSIdNxkBOowc_tIPcf0X_ydr"
+ "YBINg1pdk8Q_0";
+
+// From chrome/browser/push_messaging/push_messaging_manager.cc
+const char* kIncognitoWarningPattern =
+ "Chrome currently does not support the Push API in incognito mode "
+ "(https://crbug.com/401439). There is deliberately no way to "
+ "feature-detect this, since incognito mode needs to be undetectable by "
+ "websites.";
+
+std::string GetTestApplicationServerKey(bool base64_url_encoded = false) {
+ std::string application_server_key;
+
+ if (base64_url_encoded) {
+ base::Base64UrlEncode(reinterpret_cast<const char*>(kApplicationServerKey),
+ base::Base64UrlEncodePolicy::OMIT_PADDING,
+ &application_server_key);
+ } else {
+ application_server_key =
+ std::string(kApplicationServerKey,
+ kApplicationServerKey + base::size(kApplicationServerKey));
+ }
+
+ return application_server_key;
+}
+
+void LegacyRegisterCallback(base::OnceClosure done_callback,
+ std::string* out_registration_id,
+ gcm::GCMClient::Result* out_result,
+ const std::string& registration_id,
+ gcm::GCMClient::Result result) {
+ if (out_registration_id)
+ *out_registration_id = registration_id;
+ if (out_result)
+ *out_result = result;
+ std::move(done_callback).Run();
+}
+
+void DidRegister(base::OnceClosure done_callback,
+ const std::string& registration_id,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh,
+ const std::vector<uint8_t>& auth,
+ blink::mojom::PushRegistrationStatus status) {
+ EXPECT_EQ(blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE,
+ status);
+ std::move(done_callback).Run();
+}
+
+void InstanceIDResultCallback(base::OnceClosure done_callback,
+ instance_id::InstanceID::Result* out_result,
+ instance_id::InstanceID::Result result) {
+ DCHECK(out_result);
+ *out_result = result;
+ std::move(done_callback).Run();
+}
+
+} // namespace
+
+class PushMessagingBrowserTestBase : public InProcessBrowserTest {
+ public:
+ PushMessagingBrowserTestBase()
+ : scoped_testing_factory_installer_(
+ base::BindRepeating(&gcm::FakeGCMProfileService::Build)),
+ gcm_service_(nullptr),
+ gcm_driver_(nullptr) {}
+
+ ~PushMessagingBrowserTestBase() override = default;
+
+ PushMessagingBrowserTestBase(const PushMessagingBrowserTestBase&) = delete;
+ PushMessagingBrowserTestBase& operator=(const PushMessagingBrowserTestBase&) =
+ delete;
+
+ // InProcessBrowserTest:
+ void SetUp() override {
+ https_server_ = std::make_unique<net::EmbeddedTestServer>(
+ net::EmbeddedTestServer::TYPE_HTTPS);
+ https_server_->ServeFilesFromSourceDirectory(GetChromeTestDataDir());
+ content::SetupCrossSiteRedirector(https_server_.get());
+
+ site_engagement::SiteEngagementScore::SetParamValuesForTesting();
+ InProcessBrowserTest::SetUp();
+ }
+ void SetUpCommandLine(base::CommandLine* command_line) override {
+ // Enable experimental features for subscription restrictions.
+ command_line->AppendSwitch(
+ switches::kEnableExperimentalWebPlatformFeatures);
+
+ // HTTPS server only serves a valid cert for localhost, so this is needed to
+ // load webby domains like "embedded.com" without an interstitial.
+ command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
+ }
+
+ // InProcessBrowserTest:
+ void SetUpOnMainThread() override {
+ host_resolver()->AddRule("*", "127.0.0.1");
+ ASSERT_TRUE(https_server_->Start());
+
+ KeyedService* keyed_service =
+ gcm::GCMProfileServiceFactory::GetForProfile(GetBrowser()->profile());
+ if (keyed_service) {
+ gcm_service_ = static_cast<gcm::FakeGCMProfileService*>(keyed_service);
+ gcm_driver_ = static_cast<instance_id::FakeGCMDriverForInstanceID*>(
+ gcm_service_->driver());
+ }
+
+ notification_tester_ = std::make_unique<NotificationDisplayServiceTester>(
+ GetBrowser()->profile());
+
+ push_service_ =
+ PushMessagingServiceFactory::GetForProfile(GetBrowser()->profile());
+
+ LoadTestPage();
+ }
+
+ void TearDownOnMainThread() override {
+ notification_tester_.reset();
+ InProcessBrowserTest::TearDownOnMainThread();
+ }
+
+ // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro.
+ void RestartPushService() {
+ Profile* profile = GetBrowser()->profile();
+ PushMessagingServiceFactory::GetInstance()->SetTestingFactory(
+ profile, BrowserContextKeyedServiceFactory::TestingFactory());
+ ASSERT_EQ(nullptr, PushMessagingServiceFactory::GetForProfile(profile));
+ PushMessagingServiceFactory::GetInstance()->RestoreFactoryForTests(profile);
+ PushMessagingServiceImpl::InitializeForProfile(profile);
+ push_service_ = PushMessagingServiceFactory::GetForProfile(profile);
+ }
+
+ // Helper function to test if a Keep Alive is registered while avoiding the
+ // platform checks. Returns a boolean so that assertion failures are reported
+ // at the right line.
+ // Returns true when KeepAlives are not supported by the platform, or when
+ // the registration state is equal to the expectation.
+ bool IsRegisteredKeepAliveEqualTo(bool expectation) {
+#if BUILDFLAG(ENABLE_BACKGROUND_MODE)
+ return expectation ==
+ KeepAliveRegistry::GetInstance()->IsOriginRegistered(
+ KeepAliveOrigin::IN_FLIGHT_PUSH_MESSAGE);
+#else
+ return true;
+#endif
+ }
+
+ void LoadTestPage(const std::string& path) {
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(GetBrowser(),
+ https_server_->GetURL(path)));
+ }
+
+ void LoadTestPage() { LoadTestPage(GetTestURL()); }
+
+ void LoadTestPageWithoutManifest() { LoadTestPage(GetNoManifestTestURL()); }
+
+ bool RunScript(const std::string& script, std::string* result) {
+ return RunScript(script, result, nullptr);
+ }
+
+ bool RunScript(const std::string& script, std::string* result,
+ content::WebContents* web_contents) {
+ if (!web_contents)
+ web_contents = GetBrowser()->tab_strip_model()->GetActiveWebContents();
+ return content::ExecuteScriptAndExtractString(web_contents->GetMainFrame(),
+ script, result);
+ }
+
+ gcm::GCMAppHandler* GetAppHandler() {
+ return gcm_driver_->GetAppHandler(kPushMessagingAppIdentifierPrefix);
+ }
+
+ permissions::PermissionRequestManager* GetPermissionRequestManager() {
+ return permissions::PermissionRequestManager::FromWebContents(
+ GetBrowser()->tab_strip_model()->GetActiveWebContents());
+ }
+
+ // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro.
+ void RequestAndAcceptPermission();
+ // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro.
+ void RequestAndDenyPermission();
+
+ // Sets out_token to the subscription token (not including server URL).
+ // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro.
+ void SubscribeSuccessfully(
+ PushSubscriptionKeyFormat key_format = PushSubscriptionKeyFormat::kBinary,
+ std::string* out_token = nullptr);
+
+ // Sets up the state corresponding to a dangling push subscription whose
+ // service worker registration no longer exists. Some users may be left with
+ // such orphaned subscriptions due to service worker unregistrations not
+ // clearing push subscriptions in the past. This allows us to emulate that.
+ // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro.
+ void SetupOrphanedPushSubscription(std::string* out_app_id);
+
+ // Legacy subscribe path using GCMDriver rather than Instance IDs. Only
+ // for testing that we maintain support for existing stored registrations.
+ // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro.
+ void LegacySubscribeSuccessfully(std::string* out_subscription_id = nullptr);
+
+ // Strips server URL from a registration endpoint to get subscription token.
+ // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro.
+ void EndpointToToken(const std::string& endpoint,
+ bool standard_protocol = true,
+ std::string* out_token = nullptr);
+
+ blink::mojom::PushSubscriptionPtr GetSubscriptionForAppIdentifier(
+ const PushMessagingAppIdentifier& app_identifier) {
+ blink::mojom::PushSubscriptionPtr result;
+ base::RunLoop run_loop;
+ push_service_->GetPushSubscriptionFromAppIdentifier(
+ app_identifier,
+ base::BindLambdaForTesting(
+ [&](blink::mojom::PushSubscriptionPtr subscription) {
+ result = std::move(subscription);
+ run_loop.Quit();
+ }));
+ run_loop.Run();
+ return result;
+ }
+
+ // Deletes an Instance ID from the GCM Store but keeps the push subscription
+ // stored in the PushMessagingAppIdentifier map and Service Worker DB.
+ // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro.
+ void DeleteInstanceIDAsIfGCMStoreReset(const std::string& app_id);
+
+ PushMessagingAppIdentifier GetAppIdentifierForServiceWorkerRegistration(
+ int64_t service_worker_registration_id);
+
+ void SendMessageAndWaitUntilHandled(
+ const PushMessagingAppIdentifier& app_identifier,
+ const gcm::IncomingMessage& message);
+
+ net::EmbeddedTestServer* https_server() const { return https_server_.get(); }
+
+ // Returns a vector of the currently displayed Notification objects.
+ std::vector<message_center::Notification> GetDisplayedNotifications() {
+ return notification_tester_->GetDisplayedNotificationsForType(
+ NotificationHandler::Type::WEB_PERSISTENT);
+ }
+
+ // Returns the number of notifications that are currently being shown.
+ size_t GetNotificationCount() { return GetDisplayedNotifications().size(); }
+
+ // Removes all shown notifications.
+ void RemoveAllNotifications() {
+ notification_tester_->RemoveAllNotifications(
+ NotificationHandler::Type::WEB_PERSISTENT, true /* by_user */);
+ }
+
+ // To be called when delivery of a push message has finished. The |run_loop|
+ // will be told to quit after |messages_required| messages were received.
+ void OnDeliveryFinished(std::vector<size_t>* number_of_notifications_shown,
+ base::OnceClosure done_closure) {
+ DCHECK(number_of_notifications_shown);
+ number_of_notifications_shown->push_back(GetNotificationCount());
+
+ std::move(done_closure).Run();
+ }
+
+ PushMessagingServiceImpl* push_service() const { return push_service_; }
+
+ void SetSiteEngagementScore(const GURL& url, double score) {
+ site_engagement::SiteEngagementService* service =
+ site_engagement::SiteEngagementService::Get(GetBrowser()->profile());
+ service->ResetBaseScoreForURL(url, score);
+ EXPECT_EQ(score, service->GetScore(url));
+ }
+
+ // Matches |tag| against the notification's ID to see if the notification's
+ // js-provided tag could have been |tag|. This is not perfect as it might
+ // return true for a |tag| that is a substring of the original tag.
+ static bool TagEquals(const message_center::Notification& notification,
+ const std::string& tag) {
+ return std::string::npos != notification.id().find(tag);
+ }
+
+ protected:
+ virtual std::string GetTestURL() { return "/push_messaging/test.html"; }
+
+ virtual std::string GetNoManifestTestURL() {
+ return "/push_messaging/test_no_manifest.html";
+ }
+
+ virtual Browser* GetBrowser() const { return browser(); }
+
+ gcm::GCMProfileServiceFactory::ScopedTestingFactoryInstaller
+ scoped_testing_factory_installer_;
+
+ raw_ptr<gcm::FakeGCMProfileService> gcm_service_;
+ raw_ptr<instance_id::FakeGCMDriverForInstanceID> gcm_driver_;
+ base::HistogramTester histogram_tester_;
+
+ std::unique_ptr<NotificationDisplayServiceTester> notification_tester_;
+
+ private:
+ std::unique_ptr<net::EmbeddedTestServer> https_server_;
+ raw_ptr<PushMessagingServiceImpl> push_service_;
+};
+
+void PushMessagingBrowserTestBase::RequestAndAcceptPermission() {
+ std::string script_result;
+ GetPermissionRequestManager()->set_auto_response_for_test(
+ permissions::PermissionRequestManager::ACCEPT_ALL);
+ ASSERT_TRUE(RunScript("requestNotificationPermission();", &script_result));
+ ASSERT_EQ("permission status - granted", script_result);
+}
+
+void PushMessagingBrowserTestBase::RequestAndDenyPermission() {
+ std::string script_result;
+ GetPermissionRequestManager()->set_auto_response_for_test(
+ permissions::PermissionRequestManager::DENY_ALL);
+ ASSERT_TRUE(RunScript("requestNotificationPermission();", &script_result));
+ ASSERT_EQ("permission status - denied", script_result);
+}
+
+void PushMessagingBrowserTestBase::SubscribeSuccessfully(
+ PushSubscriptionKeyFormat key_format,
+ std::string* out_token) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ switch (key_format) {
+ case PushSubscriptionKeyFormat::kBinary:
+ ASSERT_TRUE(RunScript("removeManifest()", &script_result));
+ ASSERT_EQ("manifest removed", script_result);
+
+ ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result));
+ ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result, true, out_token));
+ break;
+ case PushSubscriptionKeyFormat::kBase64UrlEncoded:
+ ASSERT_TRUE(RunScript("removeManifest()", &script_result));
+ ASSERT_EQ("manifest removed", script_result);
+
+ ASSERT_TRUE(RunScript("documentSubscribePushWithBase64URLEncodedString()",
+ &script_result));
+ ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result, true, out_token));
+ break;
+ case PushSubscriptionKeyFormat::kOmitKey:
+ // Test backwards compatibility with old ID based subscriptions.
+ ASSERT_TRUE(
+ RunScript("documentSubscribePushWithoutKey()", &script_result));
+ ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result, false, out_token));
+ break;
+ default:
+ NOTREACHED();
+ }
+}
+
+void PushMessagingBrowserTestBase::SetupOrphanedPushSubscription(
+ std::string* out_app_id) {
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+ GURL requesting_origin =
+ https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ // Use 1234LL as it's unlikely to collide with an active service worker
+ // registration id (they increment from 0).
+ const int64_t service_worker_registration_id = 1234LL;
+
+ auto options = blink::mojom::PushSubscriptionOptions::New();
+ options->user_visible_only = true;
+
+ std::string test_application_server_key = GetTestApplicationServerKey();
+ options->application_server_key = std::vector<uint8_t>(
+ test_application_server_key.begin(), test_application_server_key.end());
+
+ base::RunLoop run_loop;
+ push_service()->SubscribeFromWorker(
+ requesting_origin, service_worker_registration_id, std::move(options),
+ base::BindOnce(&DidRegister, run_loop.QuitClosure()));
+ run_loop.Run();
+
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ GetBrowser()->profile(), requesting_origin,
+ service_worker_registration_id);
+ ASSERT_FALSE(app_identifier.is_null());
+ *out_app_id = app_identifier.app_id();
+}
+
+void PushMessagingBrowserTestBase::LegacySubscribeSuccessfully(
+ std::string* out_subscription_id) {
+ // Create a non-InstanceID GCM registration. Have to directly access
+ // GCMDriver, since this codepath has been deleted from Push.
+
+ std::string script_result;
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ GURL requesting_origin =
+ https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ int64_t service_worker_registration_id = 0LL;
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::LegacyGenerateForTesting(
+ requesting_origin, service_worker_registration_id);
+ push_service_->IncreasePushSubscriptionCount(1, true /* is_pending */);
+
+ std::string subscription_id;
+ {
+ base::RunLoop run_loop;
+ gcm::GCMClient::Result register_result = gcm::GCMClient::UNKNOWN_ERROR;
+ gcm_driver_->Register(
+ app_identifier.app_id(), {kManifestSenderId},
+ base::BindOnce(&LegacyRegisterCallback, run_loop.QuitClosure(),
+ &subscription_id, &register_result));
+ run_loop.Run();
+ ASSERT_EQ(gcm::GCMClient::SUCCESS, register_result);
+ }
+
+ app_identifier.PersistToPrefs(GetBrowser()->profile());
+ push_service_->IncreasePushSubscriptionCount(1, false /* is_pending */);
+ push_service_->DecreasePushSubscriptionCount(1, true /* was_pending */);
+
+ {
+ base::RunLoop run_loop;
+ push_service_->StorePushSubscriptionForTesting(
+ GetBrowser()->profile(), requesting_origin,
+ service_worker_registration_id, subscription_id, kManifestSenderId,
+ run_loop.QuitClosure());
+ run_loop.Run();
+ }
+
+ if (out_subscription_id)
+ *out_subscription_id = subscription_id;
+}
+
+void PushMessagingBrowserTestBase::EndpointToToken(const std::string& endpoint,
+ bool standard_protocol,
+ std::string* out_token) {
+ size_t last_slash = endpoint.rfind('/');
+
+ ASSERT_EQ(kPushMessagingGcmEndpoint, endpoint.substr(0, last_slash + 1));
+
+ ASSERT_LT(last_slash + 1, endpoint.length()); // Token must not be empty.
+
+ if (out_token)
+ *out_token = endpoint.substr(last_slash + 1);
+}
+
+PushMessagingAppIdentifier
+PushMessagingBrowserTestBase::GetAppIdentifierForServiceWorkerRegistration(
+ int64_t service_worker_registration_id) {
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ GetBrowser()->profile(), origin, service_worker_registration_id);
+ EXPECT_FALSE(app_identifier.is_null());
+ return app_identifier;
+}
+
+void PushMessagingBrowserTestBase::DeleteInstanceIDAsIfGCMStoreReset(
+ const std::string& app_id) {
+ // Delete the Instance ID directly, keeping the push subscription stored in
+ // the PushMessagingAppIdentifier map and the Service Worker database. This
+ // simulates the GCM Store getting reset but failing to clear push
+ // subscriptions, either because the store got reset before
+ // 93ec793ac69a542b2213297737178a55d069fd0d (Chrome 56), or because a race
+ // condition (e.g. shutdown) prevents PushMessagingServiceImpl::OnStoreReset
+ // from clearing all subscriptions.
+ instance_id::InstanceIDProfileService* instance_id_profile_service =
+ instance_id::InstanceIDProfileServiceFactory::GetForProfile(
+ GetBrowser()->profile());
+ DCHECK(instance_id_profile_service);
+ instance_id::InstanceIDDriver* instance_id_driver =
+ instance_id_profile_service->driver();
+ DCHECK(instance_id_driver);
+ instance_id::InstanceID::Result delete_result =
+ instance_id::InstanceID::UNKNOWN_ERROR;
+ base::RunLoop run_loop;
+ instance_id_driver->GetInstanceID(app_id)->DeleteID(base::BindOnce(
+ &InstanceIDResultCallback, run_loop.QuitClosure(), &delete_result));
+ run_loop.Run();
+ ASSERT_EQ(instance_id::InstanceID::SUCCESS, delete_result);
+}
+
+void PushMessagingBrowserTestBase::SendMessageAndWaitUntilHandled(
+ const PushMessagingAppIdentifier& app_identifier,
+ const gcm::IncomingMessage& message) {
+ base::RunLoop run_loop;
+ push_service()->SetMessageCallbackForTesting(run_loop.QuitClosure());
+ push_service()->OnMessage(app_identifier.app_id(), message);
+ run_loop.Run();
+}
+
+class PushMessagingBrowserTest : public PushMessagingBrowserTestBase {
+ public:
+ PushMessagingBrowserTest() {
+ feature_list_.InitAndDisableFeature(
+ features::kPushMessagingDisallowSenderIDs);
+ }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ SubscribeWithoutKeySuccessNotificationsGranted) {
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey));
+ EXPECT_EQ(kManifestSenderId, gcm_driver_->last_gettoken_authorized_entity());
+ EXPECT_EQ(GetAppIdentifierForServiceWorkerRegistration(0LL).app_id(),
+ gcm_driver_->last_gettoken_app_id());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ SubscribeSuccessNotificationsGranted) {
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ EXPECT_EQ(kEncodedApplicationServerKey,
+ gcm_driver_->last_gettoken_authorized_entity());
+ EXPECT_EQ(GetAppIdentifierForServiceWorkerRegistration(0LL).app_id(),
+ gcm_driver_->last_gettoken_app_id());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ SubscribeSuccessNotificationsGrantedWithBase64URLKey) {
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBase64UrlEncoded));
+ EXPECT_EQ(kEncodedApplicationServerKey,
+ gcm_driver_->last_gettoken_authorized_entity());
+ EXPECT_EQ(GetAppIdentifierForServiceWorkerRegistration(0LL).app_id(),
+ gcm_driver_->last_gettoken_app_id());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ SubscribeSuccessNotificationsPrompt) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ GetPermissionRequestManager()->set_auto_response_for_test(
+ permissions::PermissionRequestManager::ACCEPT_ALL);
+ ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result));
+ // Both of these methods EXPECT that they succeed.
+ ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result));
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ SubscribeFailureNotificationsBlocked) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndDenyPermission());
+
+ ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result));
+ EXPECT_EQ("NotAllowedError - Registration failed - permission denied",
+ script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribeFailureNoManifest) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ ASSERT_TRUE(RunScript("removeManifest()", &script_result));
+ ASSERT_EQ("manifest removed", script_result);
+
+ ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, and "
+ "manifest empty or missing",
+ script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribeFailureNoSenderId) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ ASSERT_TRUE(RunScript("swapManifestNoSenderId()", &script_result));
+ ASSERT_EQ("sender id removed from manifest", script_result);
+
+ ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, and "
+ "gcm_sender_id not found in manifest",
+ script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ RegisterFailureEmptyPushSubscriptionOptions) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ ASSERT_TRUE(
+ RunScript("documentSubscribePushWithEmptyOptions()", &script_result));
+ EXPECT_EQ("NotAllowedError - Registration failed - permission denied",
+ script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribeWithInvalidation) {
+ std::string token1, token2, token3;
+
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token1));
+ ASSERT_FALSE(token1.empty());
+
+ // Repeated calls to |subscribe()| should yield the same token.
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token2));
+ ASSERT_EQ(token1, token2);
+
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ GetBrowser()->profile(),
+ https_server()->GetURL("/").DeprecatedGetOriginAsURL(),
+ 0LL /* service_worker_registration_id */);
+
+ ASSERT_FALSE(app_identifier.is_null());
+ EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_gettoken_app_id());
+
+ // Delete the InstanceID. This captures two scenarios: either the database was
+ // corrupted, or the subscription was invalidated by the server.
+ ASSERT_NO_FATAL_FAILURE(
+ DeleteInstanceIDAsIfGCMStoreReset(app_identifier.app_id()));
+
+ EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_deletetoken_app_id());
+
+ // Repeated calls to |subscribe()| will now (silently) result in a new token.
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token3));
+ ASSERT_FALSE(token3.empty());
+ EXPECT_NE(token1, token3);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribeWorker) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Try to subscribe from a worker without a key. This should fail.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, and "
+ "gcm_sender_id not found in manifest",
+ script_result);
+
+ // Now run the subscribe with a key. This should succeed.
+ ASSERT_TRUE(RunScript("workerSubscribePush()", &script_result));
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, true /* standard_protocol */));
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ SubscribeWorkerWithBase64URLEncodedString) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Try to subscribe from a worker without a key. This should fail.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, and "
+ "gcm_sender_id not found in manifest",
+ script_result);
+
+ // Now run the subscribe with a key. This should succeed.
+ ASSERT_TRUE(RunScript("workerSubscribePushWithBase64URLEncodedString()",
+ &script_result));
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, true /* standard_protocol */));
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ ResubscribeWithoutKeyAfterSubscribingWithKeyInManifest) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Run the subscription from the document without a key, this will trigger
+ // the code to read sender id from the manifest and will write it to the
+ // datastore.
+ ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result));
+ std::string token1;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token1));
+
+ ASSERT_TRUE(RunScript("removeManifest()", &script_result));
+ ASSERT_EQ("manifest removed", script_result);
+
+ // Try to resubscribe from the document without a key or manifest.
+ // This should fail.
+ ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, "
+ "and manifest empty or missing",
+ script_result);
+
+ // Now run the subscribe from the service worker without a key.
+ // In this case, the sender id should be read from the datastore.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ std::string token2;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token2));
+ EXPECT_EQ(token1, token2);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+
+ // After unsubscribing, subscribe again from the worker with no key.
+ // The sender id should again be read from the datastore, so the
+ // subcribe should succeed, and we should get a new subscription token.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ std::string token3;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token3));
+ EXPECT_NE(token1, token3);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(
+ PushMessagingBrowserTest,
+ ResubscribeWithoutKeyAfterSubscribingFromDocumentWithP256Key) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPageWithoutManifest(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Run the subscription from the document with a key.
+ ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result));
+ ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result));
+
+ // Try to resubscribe from the document without a key - should fail.
+ ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, "
+ "and manifest empty or missing",
+ script_result);
+
+ // Now try to resubscribe from the service worker without a key.
+ // This should also fail as the original key was not numeric.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, "
+ "and gcm_sender_id not found in manifest",
+ script_result);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+
+ // After unsubscribing, try to resubscribe again without a key.
+ // This should again fail.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, "
+ "and gcm_sender_id not found in manifest",
+ script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(
+ PushMessagingBrowserTest,
+ ResubscribeWithoutKeyAfterSubscribingFromWorkerWithP256Key) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPageWithoutManifest(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Run the subscribe from the service worker with a key.
+ // This should succeed.
+ ASSERT_TRUE(RunScript("workerSubscribePush()", &script_result));
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, true /* standard_protocol */));
+
+ // Try to resubscribe from the document without a key - should fail.
+ ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, "
+ "and manifest empty or missing",
+ script_result);
+
+ // Now try to resubscribe from the service worker without a key.
+ // This should also fail as the original key was not numeric.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, and "
+ "gcm_sender_id not found in manifest",
+ script_result);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+
+ // After unsubscribing, try to resubscribe again without a key.
+ // This should again fail.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, "
+ "and gcm_sender_id not found in manifest",
+ script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(
+ PushMessagingBrowserTest,
+ ResubscribeWithoutKeyAfterSubscribingFromDocumentWithNumber) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPageWithoutManifest(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Run the subscribe from the document with a numeric key.
+ // This should succeed.
+ ASSERT_TRUE(
+ RunScript("documentSubscribePushWithNumericKey()", &script_result));
+ std::string token1;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token1));
+
+ // Try to resubscribe from the document without a key - should fail.
+ ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, "
+ "and manifest empty or missing",
+ script_result);
+
+ // Now run the subscribe from the service worker without a key.
+ // In this case, the sender id should be read from the datastore.
+ // Note, we would rather this failed as we only really want to support
+ // no-key subscribes after subscribing with a numeric gcm sender id in the
+ // manifest, not a numeric applicationServerKey, but for code simplicity
+ // this case is allowed.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ std::string token2;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token2));
+ EXPECT_EQ(token1, token2);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+
+ // After unsubscribing, subscribe again from the worker with no key.
+ // The sender id should again be read from the datastore, so the
+ // subcribe should succeed, and we should get a new subscription token.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ std::string token3;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token3));
+ EXPECT_NE(token1, token3);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(
+ PushMessagingBrowserTest,
+ ResubscribeWithoutKeyAfterSubscribingFromWorkerWithNumber) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPageWithoutManifest(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Run the subscribe from the service worker with a numeric key.
+ // This should succeed.
+ ASSERT_TRUE(RunScript("workerSubscribePushWithNumericKey()", &script_result));
+ std::string token1;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token1));
+
+ // Try to resubscribe from the document without a key - should fail.
+ ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - missing applicationServerKey, "
+ "and manifest empty or missing",
+ script_result);
+
+ // Now run the subscribe from the service worker without a key.
+ // In this case, the sender id should be read from the datastore.
+ // Note, we would rather this failed as we only really want to support
+ // no-key subscribes after subscribing with a numeric gcm sender id in the
+ // manifest, not a numeric applicationServerKey, but for code simplicity
+ // this case is allowed.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ std::string token2;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token2));
+ EXPECT_EQ(token1, token2);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+
+ // After unsubscribing, subscribe again from the worker with no key.
+ // The sender id should again be read from the datastore, so the
+ // subcribe should succeed, and we should get a new subscription token.
+ ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result));
+ std::string token3;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token3));
+ EXPECT_NE(token1, token3);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, ResubscribeWithMismatchedKey) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Run the subscribe from the service worker with a key.
+ // This should succeed.
+ ASSERT_TRUE(
+ RunScript("workerSubscribePushWithNumericKey('11111')", &script_result));
+ std::string token1;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token1));
+
+ // Try to resubscribe with a different key - should fail.
+ ASSERT_TRUE(
+ RunScript("workerSubscribePushWithNumericKey('22222')", &script_result));
+ EXPECT_EQ(
+ "InvalidStateError - Registration failed - A subscription with a "
+ "different applicationServerKey (or gcm_sender_id) already exists; to "
+ "change the applicationServerKey, unsubscribe then resubscribe.",
+ script_result);
+
+ // Try to resubscribe with the original key - should succeed.
+ ASSERT_TRUE(
+ RunScript("workerSubscribePushWithNumericKey('11111')", &script_result));
+ std::string token2;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token2));
+ EXPECT_EQ(token1, token2);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+
+ // Resubscribe with a different key after unsubscribing.
+ // Should succeed, and we should get a new subscription token.
+ ASSERT_TRUE(
+ RunScript("workerSubscribePushWithNumericKey('22222')", &script_result));
+ std::string token3;
+ ASSERT_NO_FATAL_FAILURE(
+ EndpointToToken(script_result, false /* standard_protocol */, &token3));
+ EXPECT_NE(token1, token3);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribePersisted) {
+ std::string script_result;
+
+ // First, test that Service Worker registration IDs are assigned in order of
+ // registering the Service Workers, and the (fake) push subscription ids are
+ // assigned in order of push subscription (even when these orders are
+ // different).
+
+ std::string token1;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token1));
+ PushMessagingAppIdentifier sw0_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+ EXPECT_EQ(sw0_identifier.app_id(), gcm_driver_->last_gettoken_app_id());
+
+ LoadTestPage("/push_messaging/subscope1/test.html");
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ LoadTestPage("/push_messaging/subscope2/test.html");
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ // Note that we need to reload the page after registering, otherwise
+ // navigator.serviceWorker.ready is going to be resolved with the parent
+ // Service Worker which still controls the page.
+ LoadTestPage("/push_messaging/subscope2/test.html");
+ std::string token2;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token2));
+ EXPECT_NE(token1, token2);
+ PushMessagingAppIdentifier sw2_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(2LL);
+ EXPECT_EQ(sw2_identifier.app_id(), gcm_driver_->last_gettoken_app_id());
+
+ LoadTestPage("/push_messaging/subscope1/test.html");
+ std::string token3;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token3));
+ EXPECT_NE(token1, token3);
+ EXPECT_NE(token2, token3);
+ PushMessagingAppIdentifier sw1_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(1LL);
+ EXPECT_EQ(sw1_identifier.app_id(), gcm_driver_->last_gettoken_app_id());
+
+ // Now test that the Service Worker registration IDs and push subscription IDs
+ // generated above were persisted to SW storage, by checking that they are
+ // unchanged despite requesting them in a different order.
+
+ LoadTestPage("/push_messaging/subscope1/test.html");
+ std::string token4;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token4));
+ EXPECT_EQ(token3, token4);
+ EXPECT_EQ(sw1_identifier.app_id(), gcm_driver_->last_gettoken_app_id());
+
+ LoadTestPage("/push_messaging/subscope2/test.html");
+ std::string token5;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token5));
+ EXPECT_EQ(token2, token5);
+ EXPECT_EQ(sw2_identifier.app_id(), gcm_driver_->last_gettoken_app_id());
+
+ LoadTestPage();
+ std::string token6;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token6));
+ EXPECT_EQ(token1, token6);
+ EXPECT_EQ(sw0_identifier.app_id(), gcm_driver_->last_gettoken_app_id());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, AppHandlerOnlyIfSubscribed) {
+ // This test restarts the push service to simulate restarting the browser.
+
+ EXPECT_NE(push_service(), GetAppHandler());
+ ASSERT_NO_FATAL_FAILURE(RestartPushService());
+ EXPECT_NE(push_service(), GetAppHandler());
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ EXPECT_EQ(push_service(), GetAppHandler());
+ ASSERT_NO_FATAL_FAILURE(RestartPushService());
+ EXPECT_EQ(push_service(), GetAppHandler());
+
+ std::string script_result;
+
+ // Unsubscribe.
+ base::RunLoop run_loop;
+ push_service()->SetUnsubscribeCallbackForTesting(run_loop.QuitClosure());
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+ // The app handler is only guaranteed to be unregistered once the unsubscribe
+ // callback for testing has been run (PushSubscription.unsubscribe() usually
+ // resolves before that, in order to avoid blocking on network retries etc).
+ run_loop.Run();
+
+ EXPECT_NE(push_service(), GetAppHandler());
+ ASSERT_NO_FATAL_FAILURE(RestartPushService());
+ EXPECT_NE(push_service(), GetAppHandler());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventSuccess) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false));
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "testdata";
+ message.decrypted = true;
+ push_service()->OnMessage(app_identifier.app_id(), message);
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(true));
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_EQ("testdata", script_result);
+
+ // Check that we record this case in UMA.
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus.FindServiceWorker",
+ 0 /* SERVICE_WORKER_OK */, 1);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus.ServiceWorkerEvent",
+ 0 /* SERVICE_WORKER_OK */, 1);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus",
+ static_cast<int>(blink::mojom::PushEventStatus::SUCCESS), 1);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventOnShutdown) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false));
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "testdata";
+ message.decrypted = true;
+ push_service()->Observe(chrome::NOTIFICATION_APP_TERMINATING,
+ content::NotificationService::AllSources(),
+ content::NotificationService::NoDetails());
+ push_service()->OnMessage(app_identifier.app_id(), message);
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false));
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventWithoutPayload) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.decrypted = false;
+
+ push_service()->OnMessage(app_identifier.app_id(), message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_EQ("[NULL]", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, LegacyPushEvent) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ gcm::IncomingMessage message;
+ message.sender_id = kManifestSenderId;
+ message.decrypted = false;
+
+ push_service()->OnMessage(app_identifier.app_id(), message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_EQ("[NULL]", script_result);
+}
+
+// Some users may have gotten into a state in the past where they still have
+// a subscription even though the service worker was unregistered.
+// Emulate this and test a push message triggers unsubscription.
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventNoServiceWorker) {
+ std::string app_id;
+ ASSERT_NO_FATAL_FAILURE(SetupOrphanedPushSubscription(&app_id));
+
+ // Try to send a push message.
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "testdata";
+ message.decrypted = true;
+
+ base::RunLoop run_loop;
+ push_service()->SetMessageCallbackForTesting(run_loop.QuitClosure());
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false));
+ push_service()->OnMessage(app_id, message);
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(true));
+ run_loop.Run();
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false));
+
+ // No push data should have been received.
+ std::string script_result;
+ ASSERT_TRUE(RunScript("resultQueue.popImmediately()", &script_result));
+ EXPECT_EQ("null", script_result);
+
+ // Check that we record this case in UMA.
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus.FindServiceWorker",
+ 5 /* SERVICE_WORKER_ERROR_NOT_FOUND */, 1);
+ histogram_tester_.ExpectTotalCount(
+ "PushMessaging.DeliveryStatus.ServiceWorkerEvent", 0);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus",
+ static_cast<int>(blink::mojom::PushEventStatus::NO_SERVICE_WORKER), 1);
+
+ // Missing Service Workers should trigger an automatic unsubscription attempt.
+ EXPECT_EQ(app_id, gcm_driver_->last_deletetoken_app_id());
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::DELIVERY_NO_SERVICE_WORKER),
+ 1);
+
+ // |app_identifier| should no longer be stored in prefs.
+ PushMessagingAppIdentifier stored_app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(GetBrowser()->profile(), app_id);
+ EXPECT_TRUE(stored_app_identifier.is_null());
+}
+
+// Tests receiving messages for a subscription that no longer exists.
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, NoSubscription) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 1);
+
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "testdata";
+ message.decrypted = true;
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+
+ // No push data should have been received.
+ ASSERT_TRUE(RunScript("resultQueue.popImmediately()", &script_result));
+ EXPECT_EQ("null", script_result);
+
+ // Check that we record this case in UMA.
+ histogram_tester_.ExpectTotalCount(
+ "PushMessaging.DeliveryStatus.FindServiceWorker", 0);
+ histogram_tester_.ExpectTotalCount(
+ "PushMessaging.DeliveryStatus.ServiceWorkerEvent", 0);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus",
+ static_cast<int>(blink::mojom::PushEventStatus::UNKNOWN_APP_ID), 1);
+
+ // Missing subscriptions should trigger an automatic unsubscription attempt.
+ EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_deletetoken_app_id());
+ histogram_tester_.ExpectBucketCount(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::DELIVERY_UNKNOWN_APP_ID),
+ 1);
+}
+
+// Tests receiving messages for an origin that does not have permission, but
+// somehow still has a subscription (as happened in https://crbug.com/633310).
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventWithoutPermission) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Revoke notifications permission, but first disable the
+ // PushMessagingServiceImpl's OnContentSettingChanged handler so that it
+ // doesn't automatically unsubscribe, since we want to test the case where
+ // there is still a subscription.
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->RemoveObserver(push_service());
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->ClearSettingsForOneType(ContentSettingsType::NOTIFICATIONS);
+ base::RunLoop().RunUntilIdle();
+
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "testdata";
+ message.decrypted = true;
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+
+ // No push data should have been received.
+ ASSERT_TRUE(RunScript("resultQueue.popImmediately()", &script_result));
+ EXPECT_EQ("null", script_result);
+
+ // Check that we record this case in UMA.
+ histogram_tester_.ExpectTotalCount(
+ "PushMessaging.DeliveryStatus.FindServiceWorker", 0);
+ histogram_tester_.ExpectTotalCount(
+ "PushMessaging.DeliveryStatus.ServiceWorkerEvent", 0);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus",
+ static_cast<int>(blink::mojom::PushEventStatus::PERMISSION_DENIED), 1);
+
+ // Missing permission should trigger an automatic unsubscription attempt.
+ EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_deletetoken_app_id());
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ PushMessagingAppIdentifier app_identifier_afterwards =
+ PushMessagingAppIdentifier::FindByServiceWorker(GetBrowser()->profile(),
+ origin, 0LL);
+ EXPECT_TRUE(app_identifier_afterwards.is_null());
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::DELIVERY_PERMISSION_DENIED),
+ 1);
+}
+
+// https://crbug.com/458160 test is flaky on all platforms; but mostly linux.
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ DISABLED_PushEventEnforcesUserVisibleNotification) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ RemoveAllNotifications();
+ ASSERT_EQ(0u, GetNotificationCount());
+
+ // We'll need to specify the web_contents in which to eval script, since we're
+ // going to run script in a background tab.
+ content::WebContents* web_contents =
+ GetBrowser()->tab_strip_model()->GetActiveWebContents();
+
+ // Set the site engagement score for the site. Setting it to 10 means it
+ // should have a budget of 4, enough for two non-shown notification, which
+ // cost 2 each.
+ SetSiteEngagementScore(web_contents->GetLastCommittedURL(), 10.0);
+
+ // If the site is visible in an active tab, we should not force a notification
+ // to be shown. Try it twice, since we allow one mistake per 10 push events.
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.decrypted = true;
+ for (int n = 0; n < 2; n++) {
+ message.raw_data = "testdata";
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_EQ("testdata", script_result);
+ EXPECT_EQ(0u, GetNotificationCount());
+ }
+
+ // Open a blank foreground tab so site is no longer visible.
+ ui_test_utils::NavigateToURLWithDisposition(
+ GetBrowser(), GURL("about:blank"),
+ WindowOpenDisposition::NEW_FOREGROUND_TAB,
+ ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB);
+
+ // If the Service Worker push event handler shows a notification, we
+ // should not show a forced one.
+ message.raw_data = "shownotification";
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
+ EXPECT_EQ("shownotification", script_result);
+ EXPECT_EQ(1u, GetNotificationCount());
+ EXPECT_TRUE(TagEquals(GetDisplayedNotifications()[0], "push_test_tag"));
+ RemoveAllNotifications();
+
+ // If the Service Worker push event handler does not show a notification, we
+ // should show a forced one, but only once the origin is out of budget.
+ message.raw_data = "testdata";
+ for (int n = 0; n < 2; n++) {
+ // First two missed notifications shouldn't force a default one.
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
+ EXPECT_EQ("testdata", script_result);
+ EXPECT_EQ(0u, GetNotificationCount());
+ }
+
+ // Third missed notification should trigger a default notification, since the
+ // origin will be out of budget.
+ message.raw_data = "testdata";
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
+ EXPECT_EQ("testdata", script_result);
+
+ {
+ std::vector<message_center::Notification> notifications =
+ GetDisplayedNotifications();
+ ASSERT_EQ(notifications.size(), 1u);
+
+ EXPECT_TRUE(
+ TagEquals(notifications[0], kPushMessagingForcedNotificationTag));
+ EXPECT_TRUE(notifications[0].silent());
+ }
+
+ // The notification will be automatically dismissed when the developer shows
+ // a new notification themselves at a later point in time.
+ message.raw_data = "shownotification";
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
+ EXPECT_EQ("shownotification", script_result);
+
+ {
+ std::vector<message_center::Notification> notifications =
+ GetDisplayedNotifications();
+ ASSERT_EQ(notifications.size(), 1u);
+
+ EXPECT_FALSE(
+ TagEquals(notifications[0], kPushMessagingForcedNotificationTag));
+ }
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ PushEventAllowSilentPushCommandLineFlag) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+ EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_gettoken_app_id());
+ EXPECT_EQ(kEncodedApplicationServerKey,
+ gcm_driver_->last_gettoken_authorized_entity());
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ RemoveAllNotifications();
+ ASSERT_EQ(0u, GetNotificationCount());
+
+ // We'll need to specify the web_contents in which to eval script, since we're
+ // going to run script in a background tab.
+ content::WebContents* web_contents =
+ GetBrowser()->tab_strip_model()->GetActiveWebContents();
+
+ SetSiteEngagementScore(web_contents->GetLastCommittedURL(), 5.0);
+
+ ui_test_utils::NavigateToURLWithDisposition(
+ GetBrowser(), GURL("about:blank"),
+ WindowOpenDisposition::NEW_FOREGROUND_TAB,
+ ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB);
+
+ // Send a missed notification to use up the budget.
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "testdata";
+ message.decrypted = true;
+
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
+ EXPECT_EQ("testdata", script_result);
+ EXPECT_EQ(0u, GetNotificationCount());
+
+ // If the Service Worker push event handler does not show a notification, we
+ // should show a forced one providing there is no foreground tab and the
+ // origin ran out of budget.
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
+ EXPECT_EQ("testdata", script_result);
+
+ // Because the --allow-silent-push command line flag has not been passed,
+ // this should have shown a default notification.
+ {
+ std::vector<message_center::Notification> notifications =
+ GetDisplayedNotifications();
+ ASSERT_EQ(notifications.size(), 1u);
+
+ EXPECT_TRUE(
+ TagEquals(notifications[0], kPushMessagingForcedNotificationTag));
+ EXPECT_TRUE(notifications[0].silent());
+ }
+
+ RemoveAllNotifications();
+
+ // Send the message again, but this time with the -allow-silent-push command
+ // line flag set. The default notification should *not* be shown.
+ base::CommandLine::ForCurrentProcess()->AppendSwitch(
+ switches::kAllowSilentPush);
+
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
+ EXPECT_EQ("testdata", script_result);
+
+ ASSERT_EQ(0u, GetNotificationCount());
+}
+
+class PushMessagingBrowserTestWithAbusiveOriginPermissionRevocation
+ : public PushMessagingBrowserTestBase {
+ public:
+ PushMessagingBrowserTestWithAbusiveOriginPermissionRevocation() = default;
+
+ using SiteReputation = CrowdDenyPreloadData::SiteReputation;
+
+ void CreatedBrowserMainParts(
+ content::BrowserMainParts* browser_main_parts) override {
+ PushMessagingBrowserTestBase::CreatedBrowserMainParts(browser_main_parts);
+
+ testing_preload_data_.emplace();
+ fake_database_manager_ =
+ base::MakeRefCounted<CrowdDenyFakeSafeBrowsingDatabaseManager>();
+ test_safe_browsing_factory_ =
+ std::make_unique<safe_browsing::TestSafeBrowsingServiceFactory>();
+ test_safe_browsing_factory_->SetTestDatabaseManager(
+ fake_database_manager_.get());
+ safe_browsing::SafeBrowsingServiceInterface::RegisterFactory(
+ test_safe_browsing_factory_.get());
+ }
+
+ void AddToPreloadDataBlocklist(
+ const GURL& origin,
+ chrome_browser_crowd_deny::
+ SiteReputation_NotificationUserExperienceQuality reputation_type) {
+ SiteReputation reputation;
+ reputation.set_notification_ux_quality(reputation_type);
+ testing_preload_data_->SetOriginReputation(url::Origin::Create(origin),
+ std::move(reputation));
+ }
+
+ void AddToSafeBrowsingBlocklist(const GURL& url) {
+ safe_browsing::ThreatMetadata test_metadata;
+ test_metadata.api_permissions.emplace("NOTIFICATIONS");
+ fake_database_manager_->SetSimulatedMetadataForUrl(url, test_metadata);
+ }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+ absl::optional<testing::ScopedCrowdDenyPreloadDataOverride>
+ testing_preload_data_;
+ scoped_refptr<CrowdDenyFakeSafeBrowsingDatabaseManager>
+ fake_database_manager_;
+ std::unique_ptr<safe_browsing::TestSafeBrowsingServiceFactory>
+ test_safe_browsing_factory_;
+};
+
+IN_PROC_BROWSER_TEST_F(
+ PushMessagingBrowserTestWithAbusiveOriginPermissionRevocation,
+ PushEventPermissionRevoked) {
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ LoadTestPage(); // Reload to become controlled.
+ std::string script_result;
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Add an origin to blocking lists after service worker is registered.
+ AddToPreloadDataBlocklist(
+ https_server()->GetURL("/").DeprecatedGetOriginAsURL(),
+ SiteReputation::ABUSIVE_CONTENT);
+ AddToSafeBrowsingBlocklist(
+ https_server()->GetURL("/").DeprecatedGetOriginAsURL());
+
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "testdata";
+ message.decrypted = true;
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+
+ // No push data should have been received.
+ ASSERT_TRUE(RunScript("resultQueue.popImmediately()", &script_result));
+ EXPECT_EQ("null", script_result);
+
+ // Check that we record this case in UMA.
+ histogram_tester_.ExpectTotalCount(
+ "PushMessaging.DeliveryStatus.FindServiceWorker", 0);
+ histogram_tester_.ExpectTotalCount(
+ "PushMessaging.DeliveryStatus.ServiceWorkerEvent", 0);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus",
+ static_cast<int>(
+ blink::mojom::PushEventStatus::PERMISSION_REVOKED_ABUSIVE),
+ 1);
+
+ // Missing permission should trigger an automatic unsubscription attempt.
+ EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_deletetoken_app_id());
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ PushMessagingAppIdentifier app_identifier_afterwards =
+ PushMessagingAppIdentifier::FindByServiceWorker(GetBrowser()->profile(),
+ origin, 0LL);
+ EXPECT_TRUE(app_identifier_afterwards.is_null());
+
+ // 1st event - blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED.
+ // 2nd event -
+ // blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED_ABUSIVE.
+ histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 2);
+
+ histogram_tester_.ExpectBucketCount(
+ "PushMessaging.UnregistrationReason",
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED_ABUSIVE, 1);
+ histogram_tester_.ExpectBucketCount(
+ "PushMessaging.UnregistrationReason",
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED, 1);
+}
+
+// That test verifies that an origin is not revoked because it is not on
+// SafeBrowsing blocking list.
+IN_PROC_BROWSER_TEST_F(
+ PushMessagingBrowserTestWithAbusiveOriginPermissionRevocation,
+ OriginIsNotOnSafeBrowsingBlockingList) {
+ std::string script_result;
+
+ // The origin should be marked as |ABUSIVE_CONTENT| on |CrowdDenyPreloadData|
+ // otherwise the permission revocation logic will not be triggered.
+ AddToPreloadDataBlocklist(
+ https_server()->GetURL("/").DeprecatedGetOriginAsURL(),
+ SiteReputation::ABUSIVE_CONTENT);
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false));
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "testdata";
+ message.decrypted = true;
+ push_service()->OnMessage(app_identifier.app_id(), message);
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(true));
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_EQ("testdata", script_result);
+
+ // Check that we record this case in UMA.
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus.FindServiceWorker",
+ 0 /* SERVICE_WORKER_OK */, 1);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus.ServiceWorkerEvent",
+ 0 /* SERVICE_WORKER_OK */, 1);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.DeliveryStatus",
+ static_cast<int>(blink::mojom::PushEventStatus::SUCCESS), 1);
+}
+
+class PushMessagingBrowserTestWithNotificationTriggersEnabled
+ : public PushMessagingBrowserTestBase {
+ public:
+ PushMessagingBrowserTestWithNotificationTriggersEnabled() {
+ feature_list_.InitAndEnableFeature(features::kNotificationTriggers);
+ }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTestWithNotificationTriggersEnabled,
+ PushEventIgnoresScheduledNotificationsForEnforcement) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ LoadTestPage(); // Reload to become controlled.
+
+ RemoveAllNotifications();
+
+ // We'll need to specify the web_contents in which to eval script, since we're
+ // going to run script in a background tab.
+ content::WebContents* web_contents =
+ GetBrowser()->tab_strip_model()->GetActiveWebContents();
+
+ // Initialize site engagement score to have no budget for silent pushes.
+ SetSiteEngagementScore(web_contents->GetLastCommittedURL(), 0);
+
+ ui_test_utils::NavigateToURLWithDisposition(
+ GetBrowser(), GURL("about:blank"),
+ WindowOpenDisposition::NEW_FOREGROUND_TAB,
+ ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB);
+
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "shownotification-with-showtrigger";
+ message.decrypted = true;
+
+ // If the Service Worker push event handler only schedules a notification, we
+ // should show a forced one providing there is no foreground tab and the
+ // origin ran out of budget.
+ SendMessageAndWaitUntilHandled(app_identifier, message);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
+ EXPECT_EQ("shownotification-with-showtrigger", script_result);
+
+ // Because scheduled notifications do not count as displayed notifications,
+ // this should have shown a default notification.
+ std::vector<message_center::Notification> notifications =
+ GetDisplayedNotifications();
+ ASSERT_EQ(notifications.size(), 1u);
+
+ EXPECT_TRUE(TagEquals(notifications[0], kPushMessagingForcedNotificationTag));
+ EXPECT_TRUE(notifications[0].silent());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ PushEventEnforcesUserVisibleNotificationAfterQueue) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Fire off two push messages in sequence, only the second one of which will
+ // display a notification. The additional round-trip and I/O required by the
+ // second message, which shows a notification, should give us a reasonable
+ // confidence that the ordering will be maintained.
+
+ std::vector<size_t> number_of_notifications_shown;
+
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.decrypted = true;
+
+ {
+ base::RunLoop run_loop;
+ push_service()->SetMessageCallbackForTesting(base::BindRepeating(
+ &PushMessagingBrowserTestBase::OnDeliveryFinished,
+ base::Unretained(this), &number_of_notifications_shown,
+ base::BarrierClosure(2 /* num_closures */, run_loop.QuitClosure())));
+
+ message.raw_data = "testdata";
+ push_service()->OnMessage(app_identifier.app_id(), message);
+
+ message.raw_data = "shownotification";
+ push_service()->OnMessage(app_identifier.app_id(), message);
+
+ run_loop.Run();
+ }
+
+ ASSERT_EQ(2u, number_of_notifications_shown.size());
+ EXPECT_EQ(0u, number_of_notifications_shown[0]);
+ EXPECT_EQ(1u, number_of_notifications_shown[1]);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ PushEventNotificationWithoutEventWaitUntil) {
+ std::string script_result;
+ content::WebContents* web_contents =
+ GetBrowser()->tab_strip_model()->GetActiveWebContents();
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ base::RunLoop run_loop;
+ base::RepeatingClosure quit_barrier =
+ base::BarrierClosure(2 /* num_closures */, run_loop.QuitClosure());
+ push_service()->SetMessageCallbackForTesting(quit_barrier);
+ notification_tester_->SetNotificationAddedClosure(quit_barrier);
+
+ gcm::IncomingMessage message;
+ message.sender_id = GetTestApplicationServerKey();
+ message.raw_data = "shownotification-without-waituntil";
+ message.decrypted = true;
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false));
+ push_service()->OnMessage(app_identifier.app_id(), message);
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(true));
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
+ EXPECT_EQ("immediate:shownotification-without-waituntil", script_result);
+
+ run_loop.Run();
+
+ EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false));
+ ASSERT_EQ(1u, GetNotificationCount());
+ EXPECT_TRUE(TagEquals(GetDisplayedNotifications()[0], "push_test_tag"));
+
+ // Verify that the renderer process hasn't crashed.
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PermissionStateSaysPrompt) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ ASSERT_EQ("permission status - prompt", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PermissionStateSaysGranted) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result));
+ ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result));
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PermissionStateSaysDenied) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndDenyPermission());
+
+ ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result));
+ EXPECT_EQ("NotAllowedError - Registration failed - permission denied",
+ script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - denied", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, CrossOriginFrame) {
+ const GURL kEmbedderURL = https_server()->GetURL(
+ "embedder.com", "/push_messaging/framed_test.html");
+ const GURL kRequesterURL = https_server()->GetURL("requester.com", "/");
+
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(GetBrowser(), kEmbedderURL));
+
+ auto* web_contents = GetBrowser()->tab_strip_model()->GetActiveWebContents();
+ LOG(ERROR) << web_contents->GetLastCommittedURL();
+ auto* subframe = content::ChildFrameAt(web_contents->GetMainFrame(), 0u);
+ ASSERT_TRUE(subframe);
+
+ // A cross-origin subframe that had not been granted the NOTIFICATIONS
+ // permission previously should see it as "denied", not be able to request it,
+ // and not be able to use the Push and Web Notification API. It is verified
+ // that no prompts are shown by auto-accepting and still expecting the
+ // permission to be denied.
+
+ GetPermissionRequestManager()->set_auto_response_for_test(
+ permissions::PermissionRequestManager::ACCEPT_ALL);
+
+ std::string script_result;
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "requestNotificationPermission();", &script_result));
+ EXPECT_EQ("permission status - denied", script_result);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - denied", script_result);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "notificationPermissionState()", &script_result));
+ EXPECT_EQ("permission status - denied", script_result);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "notificationPermissionAPIState()", &script_result));
+ EXPECT_EQ("permission status - denied", script_result);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "documentSubscribePush()", &script_result));
+ EXPECT_EQ("NotAllowedError - Registration failed - permission denied",
+ script_result);
+
+ // A cross-origin subframe that had been granted the NOTIFICATIONS permission
+ // previously (in a first-party context) should see it as "granted", and be
+ // able to use the Push and Web Notifications APIs.
+
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->SetContentSettingDefaultScope(kRequesterURL, kRequesterURL,
+ ContentSettingsType::NOTIFICATIONS,
+ CONTENT_SETTING_ALLOW);
+
+ GetPermissionRequestManager()->set_auto_response_for_test(
+ permissions::PermissionRequestManager::DENY_ALL);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "requestNotificationPermission();", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "notificationPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "notificationPermissionAPIState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ subframe, "documentSubscribePush()", &script_result));
+ ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result));
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, UnsubscribeSuccess) {
+ std::string script_result;
+
+ std::string token1;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey, &token1));
+ ASSERT_TRUE(RunScript("storePushSubscription()", &script_result));
+ EXPECT_EQ("ok - stored", script_result);
+
+ // Resolves true if there was a subscription.
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 1);
+
+ // Resolves false if there was no longer a subscription.
+ ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result));
+ EXPECT_EQ("unsubscribe result: false", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 2);
+
+ // TODO(johnme): Test that doesn't reject if there was a network error (should
+ // deactivate subscription locally anyway).
+ // TODO(johnme): Test that doesn't reject if there were other push service
+ // errors (should deactivate subscription locally anyway).
+
+ // Unsubscribing (with an existing reference to a PushSubscription), after
+ // replacing the Service Worker, actually still works, as the Service Worker
+ // registration is unchanged.
+ std::string token2;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey, &token2));
+ EXPECT_NE(token1, token2);
+ ASSERT_TRUE(RunScript("storePushSubscription()", &script_result));
+ EXPECT_EQ("ok - stored", script_result);
+ ASSERT_TRUE(RunScript("replaceServiceWorker()", &script_result));
+ EXPECT_EQ("ok - service worker replaced", script_result);
+ ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 3);
+
+ // Unsubscribing (with an existing reference to a PushSubscription), after
+ // unregistering the Service Worker, should fail.
+ std::string token3;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey, &token3));
+ EXPECT_NE(token1, token3);
+ EXPECT_NE(token2, token3);
+ ASSERT_TRUE(RunScript("storePushSubscription()", &script_result));
+ EXPECT_EQ("ok - stored", script_result);
+
+ // Unregister service worker and wait for callback.
+ base::RunLoop run_loop;
+ push_service()->SetServiceWorkerUnregisteredCallbackForTesting(
+ run_loop.QuitClosure());
+ ASSERT_TRUE(RunScript("unregisterServiceWorker()", &script_result));
+ EXPECT_EQ("service worker unregistration status: true", script_result);
+ run_loop.Run();
+
+ // Unregistering should have triggered an automatic unsubscribe.
+ histogram_tester_.ExpectBucketCount(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::SERVICE_WORKER_UNREGISTERED),
+ 1);
+ histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 4);
+
+ // Now manual unsubscribe should return false.
+ ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result));
+ EXPECT_EQ("unsubscribe result: false", script_result);
+}
+
+// Push subscriptions used to be non-InstanceID GCM registrations. Still need
+// to be able to unsubscribe these, even though new ones are no longer created.
+// Flaky on some Win and Linux buildbots. See crbug.com/835382.
+#if defined(OS_WIN) || defined(OS_LINUX) || defined(OS_CHROMEOS)
+#define MAYBE_LegacyUnsubscribeSuccess DISABLED_LegacyUnsubscribeSuccess
+#else
+#define MAYBE_LegacyUnsubscribeSuccess LegacyUnsubscribeSuccess
+#endif
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ MAYBE_LegacyUnsubscribeSuccess) {
+ std::string script_result;
+
+ std::string subscription_id1;
+ ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id1));
+ ASSERT_TRUE(RunScript("storePushSubscription()", &script_result));
+ EXPECT_EQ("ok - stored", script_result);
+
+ // Resolves true if there was a subscription.
+ gcm_service_->AddExpectedUnregisterResponse(gcm::GCMClient::SUCCESS);
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 1);
+
+ // Resolves false if there was no longer a subscription.
+ ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result));
+ EXPECT_EQ("unsubscribe result: false", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 2);
+
+ // Doesn't reject if there was a network error (deactivates subscription
+ // locally anyway).
+ std::string subscription_id2;
+ ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id2));
+ EXPECT_NE(subscription_id1, subscription_id2);
+ gcm_service_->AddExpectedUnregisterResponse(gcm::GCMClient::NETWORK_ERROR);
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 3);
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+
+ // Doesn't reject if there were other push service errors (deactivates
+ // subscription locally anyway).
+ std::string subscription_id3;
+ ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id3));
+ EXPECT_NE(subscription_id1, subscription_id3);
+ EXPECT_NE(subscription_id2, subscription_id3);
+ gcm_service_->AddExpectedUnregisterResponse(
+ gcm::GCMClient::INVALID_PARAMETER);
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 4);
+
+ // Unsubscribing (with an existing reference to a PushSubscription), after
+ // replacing the Service Worker, actually still works, as the Service Worker
+ // registration is unchanged.
+ std::string subscription_id4;
+ ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id4));
+ EXPECT_NE(subscription_id1, subscription_id4);
+ EXPECT_NE(subscription_id2, subscription_id4);
+ EXPECT_NE(subscription_id3, subscription_id4);
+ ASSERT_TRUE(RunScript("storePushSubscription()", &script_result));
+ EXPECT_EQ("ok - stored", script_result);
+ ASSERT_TRUE(RunScript("replaceServiceWorker()", &script_result));
+ EXPECT_EQ("ok - service worker replaced", script_result);
+ ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 5);
+
+ // Unsubscribing (with an existing reference to a PushSubscription), after
+ // unregistering the Service Worker, should fail.
+ std::string subscription_id5;
+ ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id5));
+ EXPECT_NE(subscription_id1, subscription_id5);
+ EXPECT_NE(subscription_id2, subscription_id5);
+ EXPECT_NE(subscription_id3, subscription_id5);
+ EXPECT_NE(subscription_id4, subscription_id5);
+ ASSERT_TRUE(RunScript("storePushSubscription()", &script_result));
+ EXPECT_EQ("ok - stored", script_result);
+
+ // Unregister service worker and wait for callback.
+ base::RunLoop run_loop;
+ push_service()->SetServiceWorkerUnregisteredCallbackForTesting(
+ run_loop.QuitClosure());
+ ASSERT_TRUE(RunScript("unregisterServiceWorker()", &script_result));
+ EXPECT_EQ("service worker unregistration status: true", script_result);
+ run_loop.Run();
+
+ // Unregistering should have triggered an automatic unsubscribe.
+ histogram_tester_.ExpectBucketCount(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::SERVICE_WORKER_UNREGISTERED),
+ 1);
+ histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 6);
+
+ // Now manual unsubscribe should return false.
+ ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result));
+ EXPECT_EQ("unsubscribe result: false", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, UnsubscribeOffline) {
+ std::string script_result;
+
+ EXPECT_NE(push_service(), GetAppHandler());
+
+ std::string token;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token));
+
+ gcm_service_->set_offline(true);
+
+ // Should quickly resolve true after deleting local state (rather than waiting
+ // until unsubscribing over the network exceeds the maximum backoff duration).
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API),
+ 1);
+
+ // Since the service is offline, the network request to GCM is still being
+ // retried, so the app handler shouldn't have been unregistered yet.
+ EXPECT_EQ(push_service(), GetAppHandler());
+ // But restarting the push service will unregister the app handler, since the
+ // subscription is no longer stored in the PushMessagingAppIdentifier map.
+ ASSERT_NO_FATAL_FAILURE(RestartPushService());
+ EXPECT_NE(push_service(), GetAppHandler());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ UnregisteringServiceWorkerUnsubscribes) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Unregister the worker, and wait for callback to complete.
+ base::RunLoop run_loop;
+ push_service()->SetServiceWorkerUnregisteredCallbackForTesting(
+ run_loop.QuitClosure());
+ ASSERT_TRUE(RunScript("unregisterServiceWorker()", &script_result));
+ ASSERT_EQ("service worker unregistration status: true", script_result);
+ run_loop.Run();
+
+ // This should have unregistered the push subscription.
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::SERVICE_WORKER_UNREGISTERED),
+ 1);
+
+ // We should not be able to look up the app id.
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ GetBrowser()->profile(), origin,
+ 0LL /* service_worker_registration_id */);
+ EXPECT_TRUE(app_identifier.is_null());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ ServiceWorkerDatabaseDeletionUnsubscribes) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Pretend as if the Service Worker database went away, and wait for callback
+ // to complete.
+ base::RunLoop run_loop;
+ push_service()->SetServiceWorkerDatabaseWipedCallbackForTesting(
+ run_loop.QuitClosure());
+ push_service()->DidDeleteServiceWorkerDatabase();
+ run_loop.Run();
+
+ // This should have unregistered the push subscription.
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::
+ SERVICE_WORKER_DATABASE_WIPED),
+ 1);
+
+ // There should not be any subscriptions left.
+ EXPECT_EQ(PushMessagingAppIdentifier::GetCount(GetBrowser()->profile()), 0u);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ InvalidGetSubscriptionUnsubscribes) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ PushMessagingAppIdentifier app_identifier1 =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ GetBrowser()->profile(), origin,
+ 0LL /* service_worker_registration_id */);
+ ASSERT_FALSE(app_identifier1.is_null());
+
+ ASSERT_NO_FATAL_FAILURE(
+ DeleteInstanceIDAsIfGCMStoreReset(app_identifier1.app_id()));
+
+ // Push messaging should not yet be aware of the InstanceID being deleted.
+ histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 0);
+ // We should still be able to look up the app id.
+ PushMessagingAppIdentifier app_identifier2 =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ GetBrowser()->profile(), origin,
+ 0LL /* service_worker_registration_id */);
+ EXPECT_FALSE(app_identifier2.is_null());
+ EXPECT_EQ(app_identifier1.app_id(), app_identifier2.app_id());
+
+ // Now call PushManager.getSubscription(). It should return null.
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+
+ // This should have unsubscribed the push subscription.
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(blink::mojom::PushUnregistrationReason::
+ GET_SUBSCRIPTION_STORAGE_CORRUPT),
+ 1);
+ // We should no longer be able to look up the app id.
+ PushMessagingAppIdentifier app_identifier3 =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ GetBrowser()->profile(), origin,
+ 0LL /* service_worker_registration_id */);
+ EXPECT_TRUE(app_identifier3.is_null());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ GlobalResetPushPermissionUnsubscribes) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ scoped_refptr<content::MessageLoopRunner> message_loop_runner =
+ new content::MessageLoopRunner;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ base::BarrierClosure(1, message_loop_runner->QuitClosure()));
+
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->ClearSettingsForOneType(ContentSettingsType::NOTIFICATIONS);
+
+ message_loop_runner->Run();
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - prompt", script_result);
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED),
+ 1);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ LocalResetPushPermissionUnsubscribes) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ scoped_refptr<content::MessageLoopRunner> message_loop_runner =
+ new content::MessageLoopRunner;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ base::BarrierClosure(1, message_loop_runner->QuitClosure()));
+
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->SetContentSettingDefaultScope(origin, origin,
+ ContentSettingsType::NOTIFICATIONS,
+ CONTENT_SETTING_DEFAULT);
+
+ message_loop_runner->Run();
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - prompt", script_result);
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED),
+ 1);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ DenyPushPermissionUnsubscribes) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ scoped_refptr<content::MessageLoopRunner> message_loop_runner =
+ new content::MessageLoopRunner;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ base::BarrierClosure(1, message_loop_runner->QuitClosure()));
+
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->SetContentSettingDefaultScope(origin, origin,
+ ContentSettingsType::NOTIFICATIONS,
+ CONTENT_SETTING_BLOCK);
+
+ message_loop_runner->Run();
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - denied", script_result);
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED),
+ 1);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ GlobalResetNotificationsPermissionUnsubscribes) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ scoped_refptr<content::MessageLoopRunner> message_loop_runner =
+ new content::MessageLoopRunner;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ base::BarrierClosure(1, message_loop_runner->QuitClosure()));
+
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->ClearSettingsForOneType(ContentSettingsType::NOTIFICATIONS);
+
+ message_loop_runner->Run();
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - prompt", script_result);
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED),
+ 1);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ LocalResetNotificationsPermissionUnsubscribes) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ scoped_refptr<content::MessageLoopRunner> message_loop_runner =
+ new content::MessageLoopRunner;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ base::BarrierClosure(1, message_loop_runner->QuitClosure()));
+
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->SetContentSettingDefaultScope(origin, GURL(),
+ ContentSettingsType::NOTIFICATIONS,
+ CONTENT_SETTING_DEFAULT);
+
+ message_loop_runner->Run();
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - prompt", script_result);
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED),
+ 1);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ DenyNotificationsPermissionUnsubscribes) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ scoped_refptr<content::MessageLoopRunner> message_loop_runner =
+ new content::MessageLoopRunner;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ base::BarrierClosure(1, message_loop_runner->QuitClosure()));
+
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->SetContentSettingDefaultScope(origin, GURL(),
+ ContentSettingsType::NOTIFICATIONS,
+ CONTENT_SETTING_BLOCK);
+
+ message_loop_runner->Run();
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - denied", script_result);
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("false - not subscribed", script_result);
+
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED),
+ 1);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ GrantAlreadyGrantedPermissionDoesNotUnsubscribe) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ scoped_refptr<content::MessageLoopRunner> message_loop_runner =
+ new content::MessageLoopRunner;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ base::BarrierClosure(1, message_loop_runner->QuitClosure()));
+
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->SetContentSettingDefaultScope(origin, GURL(),
+ ContentSettingsType::NOTIFICATIONS,
+ CONTENT_SETTING_ALLOW);
+
+ message_loop_runner->Run();
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 0);
+}
+
+// This test is testing some non-trivial content settings rules and make sure
+// that they are respected with regards to automatic unsubscription. In other
+// words, it checks that the push service does not end up unsubscribing origins
+// that have push permission with some non-common rules.
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
+ AutomaticUnsubscriptionFollowsContentSettingRules) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ scoped_refptr<content::MessageLoopRunner> message_loop_runner =
+ new content::MessageLoopRunner;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ base::BarrierClosure(2, message_loop_runner->QuitClosure()));
+
+ GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL();
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->SetDefaultContentSetting(ContentSettingsType::NOTIFICATIONS,
+ CONTENT_SETTING_ALLOW);
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->SetContentSettingDefaultScope(origin, GURL(),
+ ContentSettingsType::NOTIFICATIONS,
+ CONTENT_SETTING_DEFAULT);
+
+ message_loop_runner->Run();
+
+ // The two first rules should give |origin| the permission to use Push even
+ // if the rules it used to have have been reset.
+ // The Push service should not unsubscribe |origin| because at no point it was
+ // left without permission to use Push.
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 0);
+}
+
+// Checks automatically unsubscribing due to a revoked permission after
+// previously clearing site data, under legacy conditions (ie. when
+// unregistering a worker did not unsubscribe from push.)
+IN_PROC_BROWSER_TEST_F(
+ PushMessagingBrowserTest,
+ ResetPushPermissionAfterClearingSiteDataUnderLegacyConditions) {
+ std::string app_id;
+ ASSERT_NO_FATAL_FAILURE(SetupOrphanedPushSubscription(&app_id));
+
+ // Simulate a user clearing site data (including Service Workers, crucially).
+ content::BrowsingDataRemover* remover =
+ GetBrowser()->profile()->GetBrowsingDataRemover();
+ content::BrowsingDataRemoverCompletionObserver observer(remover);
+ remover->RemoveAndReply(
+ base::Time(), base::Time::Max(),
+ chrome_browsing_data_remover::DATA_TYPE_SITE_DATA,
+ content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB, &observer);
+ observer.BlockUntilCompletion();
+
+ base::RunLoop run_loop;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ run_loop.QuitClosure());
+ // This shouldn't (asynchronously) cause a DCHECK.
+ // TODO(johnme): Get this test running on Android with legacy GCM
+ // registrations, which have a different codepath due to sender_id being
+ // required for unsubscribing there.
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->ClearSettingsForOneType(ContentSettingsType::NOTIFICATIONS);
+
+ run_loop.Run();
+
+ // |app_identifier| should no longer be stored in prefs.
+ PushMessagingAppIdentifier stored_app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(GetBrowser()->profile(), app_id);
+ EXPECT_TRUE(stored_app_identifier.is_null());
+
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.UnregistrationReason",
+ static_cast<int>(
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED),
+ 1);
+
+ base::RunLoop().RunUntilIdle();
+
+ // Revoked permission should trigger an automatic unsubscription attempt.
+ EXPECT_EQ(app_id, gcm_driver_->last_deletetoken_app_id());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, EncryptionKeyUniqueness) {
+ std::string token1;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey, &token1));
+
+ std::string first_public_key;
+ ASSERT_TRUE(RunScript("GetP256dh()", &first_public_key));
+ EXPECT_GE(first_public_key.size(), 32u);
+
+ std::string script_result;
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+
+ std::string token2;
+ ASSERT_NO_FATAL_FAILURE(
+ SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token2));
+ EXPECT_NE(token1, token2);
+
+ std::string second_public_key;
+ ASSERT_TRUE(RunScript("GetP256dh()", &second_public_key));
+ EXPECT_GE(second_public_key.size(), 32u);
+
+ EXPECT_NE(first_public_key, second_public_key);
+}
+
+class PushMessagingIncognitoBrowserTest : public PushMessagingBrowserTestBase {
+ public:
+ PushMessagingIncognitoBrowserTest()
+ : prerender_helper_(base::BindRepeating(
+ &PushMessagingIncognitoBrowserTest::web_contents,
+ base::Unretained(this))) {}
+ ~PushMessagingIncognitoBrowserTest() override = default;
+
+ // PushMessagingBrowserTest:
+ void SetUpOnMainThread() override {
+ incognito_browser_ = CreateIncognitoBrowser();
+ // We SetUp here rather than in SetUp since the https_server isn't yet
+ // created at that time.
+ prerender_helper_.SetUp(https_server());
+ PushMessagingBrowserTestBase::SetUpOnMainThread();
+ }
+ Browser* GetBrowser() const override { return incognito_browser_; }
+
+ content::WebContents* web_contents() {
+ return GetBrowser()->tab_strip_model()->GetActiveWebContents();
+ }
+
+ protected:
+ content::test::PrerenderTestHelper prerender_helper_;
+ raw_ptr<Browser> incognito_browser_ = nullptr;
+};
+
+// Regression test for https://crbug.com/476474
+IN_PROC_BROWSER_TEST_F(PushMessagingIncognitoBrowserTest,
+ IncognitoGetSubscriptionDoesNotHang) {
+ ASSERT_TRUE(GetBrowser()->profile()->IsOffTheRecord());
+
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ // In Incognito mode the promise returned by getSubscription should not hang,
+ // it should just fulfill with null.
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ ASSERT_EQ("false - not subscribed", script_result);
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingIncognitoBrowserTest, WarningToCorrectRFH) {
+ ASSERT_TRUE(GetBrowser()->profile()->IsOffTheRecord());
+
+ content::WebContentsConsoleObserver console_observer(web_contents());
+ console_observer.SetPattern(kIncognitoWarningPattern);
+
+ // Filter out the main frame host of the currently active page.
+ content::RenderFrameHost* rfh = web_contents()->GetMainFrame();
+ console_observer.SetFilter(base::BindLambdaForTesting(
+ [&](const content::WebContentsConsoleObserver::Message& message) {
+ return message.source_frame == rfh;
+ }));
+
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result));
+ ASSERT_EQ("AbortError - Registration failed - permission denied",
+ script_result);
+
+ console_observer.Wait();
+ EXPECT_EQ(1u, console_observer.messages().size());
+}
+
+IN_PROC_BROWSER_TEST_F(PushMessagingIncognitoBrowserTest,
+ WarningToCorrectRFH_Prerender) {
+ ASSERT_TRUE(GetBrowser()->profile()->IsOffTheRecord());
+
+ const GURL url(https_server()->GetURL(GetTestURL()));
+
+ // Start a prerender with the push messaging test URL.
+ int host_id = prerender_helper_.AddPrerender(url);
+ content::test::PrerenderHostObserver prerender_observer(*web_contents(),
+ host_id);
+ ASSERT_NE(prerender_helper_.GetHostForUrl(url),
+ content::RenderFrameHost::kNoFrameTreeNodeId);
+
+ content::WebContentsConsoleObserver console_observer(web_contents());
+ console_observer.SetPattern(kIncognitoWarningPattern);
+
+ // Filter out the main frame host of the prerendered page.
+ content::RenderFrameHost* prerender_rfh =
+ prerender_helper_.GetPrerenderedMainFrameHost(host_id);
+ console_observer.SetFilter(base::BindLambdaForTesting(
+ [&](const content::WebContentsConsoleObserver::Message& message) {
+ return message.source_frame == prerender_rfh;
+ }));
+
+ std::string script_result;
+
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ prerender_rfh, "registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ // Use ExecuteScriptAsync because binding of blink::mojom::PushMessaging
+ // is deferred for the prerendered page. Script execution will finish after
+ // the activation.
+ ExecuteScriptAsync(prerender_rfh, "documentSubscribePush()");
+
+ // Activate the prerendered page and wait for a response of script execution.
+ content::DOMMessageQueue message_queue;
+ prerender_helper_.NavigatePrimaryPage(url);
+ // Make sure that the prerender was activated.
+ ASSERT_TRUE(prerender_observer.was_activated());
+ do {
+ ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
+ } while (script_result !=
+ "\"AbortError - Registration failed - permission denied\"");
+
+ console_observer.Wait();
+ EXPECT_EQ(1u, console_observer.messages().size());
+}
+
+class PushMessagingDisallowSenderIdsBrowserTest
+ : public PushMessagingBrowserTestBase {
+ public:
+ PushMessagingDisallowSenderIdsBrowserTest() {
+ scoped_feature_list_.InitAndEnableFeature(
+ features::kPushMessagingDisallowSenderIDs);
+ }
+
+ ~PushMessagingDisallowSenderIdsBrowserTest() override = default;
+
+ private:
+ base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(PushMessagingDisallowSenderIdsBrowserTest,
+ SubscriptionWithSenderIdFails) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Attempt to create a subscription with a GCM Sender ID ("numeric key"),
+ // which should fail because the kPushMessagingDisallowSenderIDs feature has
+ // been enabled for this test.
+ ASSERT_TRUE(
+ RunScript("documentSubscribePushWithNumericKey()", &script_result));
+ EXPECT_EQ(
+ "AbortError - Registration failed - GCM Sender IDs are no longer "
+ "supported, please upgrade to VAPID authentication instead",
+ script_result);
+}
+
+class PushSubscriptionWithExpirationTimeTest
+ : public PushMessagingBrowserTestBase {
+ public:
+ PushSubscriptionWithExpirationTimeTest() {
+ scoped_feature_list_.InitAndEnableFeature(
+ features::kPushSubscriptionWithExpirationTime);
+ }
+
+ ~PushSubscriptionWithExpirationTimeTest() override = default;
+
+ // Checks whether |expiration_time| lies in the future and is in the
+ // valid format (seconds elapsed since Unix time)
+ bool IsExpirationTimeValid(const std::string& expiration_time);
+
+ private:
+ base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+bool PushSubscriptionWithExpirationTimeTest::IsExpirationTimeValid(
+ const std::string& expiration_time) {
+ int64_t output;
+ if (!base::StringToInt64(expiration_time, &output))
+ return false;
+ return base::Time::Now().ToJsTimeIgnoringNull() < output;
+}
+
+IN_PROC_BROWSER_TEST_F(PushSubscriptionWithExpirationTimeTest,
+ SubscribeGetSubscriptionWithExpirationTime) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // Subscribe with expiration time enabled, should get a subscription with
+ // expiration time in the future back
+ std::string subscription_expiration_time;
+ ASSERT_TRUE(RunScript("documentSubscribePushGetExpirationTime()",
+ &subscription_expiration_time));
+ EXPECT_TRUE(IsExpirationTimeValid(subscription_expiration_time));
+
+ std::string get_subscription_expiration_time;
+ // Get subscription should also yield a subscription with expiration time
+ ASSERT_TRUE(RunScript("GetSubscriptionExpirationTime()",
+ &get_subscription_expiration_time));
+ EXPECT_TRUE(IsExpirationTimeValid(get_subscription_expiration_time));
+ // Both methods should return the same expiration time
+ ASSERT_EQ(subscription_expiration_time, get_subscription_expiration_time);
+}
+
+IN_PROC_BROWSER_TEST_F(PushSubscriptionWithExpirationTimeTest,
+ GetSubscriptionWithExpirationTime) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ // Get subscription should also yield a subscription with expiration time
+ ASSERT_TRUE(RunScript("GetSubscriptionExpirationTime()", &script_result));
+ EXPECT_TRUE(IsExpirationTimeValid(script_result));
+}
+
+class PushSubscriptionWithoutExpirationTimeTest
+ : public PushMessagingBrowserTestBase {
+ public:
+ PushSubscriptionWithoutExpirationTimeTest() {
+ // Override current feature list to ensure having
+ // |kPushSubscriptionWithExpirationTime| disabled
+ scoped_feature_list_.InitAndDisableFeature(
+ features::kPushSubscriptionWithExpirationTime);
+ }
+
+ ~PushSubscriptionWithoutExpirationTimeTest() override = default;
+
+ private:
+ base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(PushSubscriptionWithoutExpirationTimeTest,
+ SubscribeDocumentExpirationTimeNull) {
+ std::string script_result;
+
+ ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result));
+ ASSERT_EQ("ok - service worker registered", script_result);
+
+ ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission());
+
+ LoadTestPage(); // Reload to become controlled.
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ // When |features::kPushSubscriptionWithExpirationTime| is disabled,
+ // expiration time should be null
+ ASSERT_TRUE(
+ RunScript("documentSubscribePushGetExpirationTime()", &script_result));
+ EXPECT_EQ("null", script_result);
+}
+
+class PushSubscriptionChangeEventTest : public PushMessagingBrowserTestBase {
+ public:
+ PushSubscriptionChangeEventTest() {
+ scoped_feature_list_.InitWithFeatures(
+ {features::kPushSubscriptionChangeEvent,
+ features::kPushSubscriptionWithExpirationTime},
+ {});
+ }
+
+ ~PushSubscriptionChangeEventTest() override = default;
+
+ private:
+ base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(PushSubscriptionChangeEventTest,
+ PushSubscriptionChangeEventSuccess) {
+ std::string script_result;
+
+ // Create the |old_subscription| by subscribing and unsubscribing again
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ blink::mojom::PushSubscriptionPtr old_subscription =
+ GetSubscriptionForAppIdentifier(app_identifier);
+
+ ASSERT_TRUE(RunScript("unsubscribePush()", &script_result));
+ EXPECT_EQ("unsubscribe result: true", script_result);
+
+ // There should be no subscription since we unsubscribed
+ EXPECT_EQ(PushMessagingAppIdentifier::GetCount(GetBrowser()->profile()), 0u);
+
+ // Create a |new_subscription| by resubscribing
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+ app_identifier = GetAppIdentifierForServiceWorkerRegistration(0LL);
+
+ blink::mojom::PushSubscriptionPtr new_subscription =
+ GetSubscriptionForAppIdentifier(app_identifier);
+
+ // Save the endpoints to compare with the JS result
+ GURL old_endpoint = old_subscription->endpoint;
+ GURL new_endpoint = new_subscription->endpoint;
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ base::RunLoop run_loop;
+ push_service()->FirePushSubscriptionChange(
+ app_identifier, run_loop.QuitClosure(), std::move(new_subscription),
+ std::move(old_subscription));
+ run_loop.Run();
+
+ // Compare old subscription
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_EQ(old_endpoint.spec(), script_result);
+ // Compare new subscription
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_EQ(new_endpoint.spec(), script_result);
+
+ // Check that we record this case in UMA.
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.PushSubscriptionChangeStatus",
+ blink::mojom::PushEventStatus::SUCCESS, 1);
+}
+
+IN_PROC_BROWSER_TEST_F(PushSubscriptionChangeEventTest,
+ FiredAfterPermissionRevoked) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - granted", script_result);
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+ auto old_subscription = GetSubscriptionForAppIdentifier(app_identifier);
+
+ base::RunLoop run_loop;
+ push_service()->SetContentSettingChangedCallbackForTesting(
+ run_loop.QuitClosure());
+ HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile())
+ ->SetContentSettingDefaultScope(app_identifier.origin(), GURL(),
+ ContentSettingsType::NOTIFICATIONS,
+ CONTENT_SETTING_BLOCK);
+ run_loop.Run();
+
+ ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result));
+ EXPECT_EQ("permission status - denied", script_result);
+
+ // Check if the pushsubscriptionchangeevent arrived in the document and
+ // whether the |old_subscription| has the expected endpoint and
+ // |new_subscription| is null
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_EQ(old_subscription->endpoint.spec(), script_result);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_EQ("null", script_result);
+
+ // Check that we record this case in UMA.
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.PushSubscriptionChangeStatus",
+ blink::mojom::PushEventStatus::SUCCESS, 1);
+}
+
+IN_PROC_BROWSER_TEST_F(PushSubscriptionChangeEventTest, OnInvalidation) {
+ std::string script_result;
+
+ ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully());
+
+ ASSERT_TRUE(RunScript("hasSubscription()", &script_result));
+ EXPECT_EQ("true - subscribed", script_result);
+
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("false - is not controlled", script_result);
+ LoadTestPage(); // Reload to become controlled.
+ ASSERT_TRUE(RunScript("isControlled()", &script_result));
+ ASSERT_EQ("true - is controlled", script_result);
+
+ PushMessagingAppIdentifier app_identifier =
+ GetAppIdentifierForServiceWorkerRegistration(0LL);
+ ASSERT_FALSE(app_identifier.is_null());
+
+ base::RunLoop run_loop;
+ push_service()->SetInvalidationCallbackForTesting(run_loop.QuitClosure());
+ push_service()->OnSubscriptionInvalidation(app_identifier.app_id());
+ run_loop.Run();
+
+ // Old subscription should be gone
+ PushMessagingAppIdentifier deleted_identifier =
+ PushMessagingAppIdentifier::FindByAppId(GetBrowser()->profile(),
+ app_identifier.app_id());
+ EXPECT_TRUE(deleted_identifier.is_null());
+
+ // New subscription with a different app id should exist
+ PushMessagingAppIdentifier new_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ GetBrowser()->profile(), app_identifier.origin(),
+ app_identifier.service_worker_registration_id());
+ EXPECT_FALSE(new_identifier.is_null());
+
+ base::RunLoop().RunUntilIdle();
+
+ // Expect `pushsubscriptionchange` event that is not null
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_NE("null", script_result);
+ ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
+ EXPECT_NE("null", script_result);
+
+ // Check that we record this case in UMA.
+ histogram_tester_.ExpectUniqueSample(
+ "PushMessaging.PushSubscriptionChangeStatus",
+ blink::mojom::PushEventStatus::SUCCESS, 1);
+}
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_constants.cc b/chromium/chrome/browser/push_messaging/push_messaging_constants.cc
new file mode 100644
index 00000000000..bd0d33d6134
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_constants.cc
@@ -0,0 +1,11 @@
+// Copyright 2014 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.
+
+#include "chrome/browser/push_messaging/push_messaging_constants.h"
+
+const char kPushMessagingGcmEndpoint[] =
+ "https://fcm.googleapis.com/fcm/send/";
+
+const char kPushMessagingForcedNotificationTag[] =
+ "user_visible_auto_notification";
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_constants.h b/chromium/chrome/browser/push_messaging/push_messaging_constants.h
new file mode 100644
index 00000000000..cf0480a5f14
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_constants.h
@@ -0,0 +1,25 @@
+// Copyright 2014 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_PUSH_MESSAGING_PUSH_MESSAGING_CONSTANTS_H_
+#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_CONSTANTS_H_
+
+#include "base/time/time.h"
+
+extern const char kPushMessagingGcmEndpoint[];
+
+// The tag of the notification that will be automatically shown if a webapp
+// receives a push message then fails to show a notification.
+extern const char kPushMessagingForcedNotificationTag[];
+
+// Chrome decided cadence on subscription refreshes. According to the standards:
+// https://w3c.github.io/push-api/#dfn-subscription-expiration-time it is
+// optional and set by the browser.
+constexpr base::TimeDelta kPushSubscriptionExpirationPeriodTimeDelta =
+ base::Days(90);
+
+// TimeDelta for subscription refreshes to keep old subscriptions alive
+constexpr base::TimeDelta kPushSubscriptionRefreshTimeDelta = base::Minutes(2);
+
+#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_CONSTANTS_H_
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_features.cc b/chromium/chrome/browser/push_messaging/push_messaging_features.cc
new file mode 100644
index 00000000000..11ad3a97bb5
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_features.cc
@@ -0,0 +1,15 @@
+// Copyright 2020 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.
+
+#include "chrome/browser/push_messaging/push_messaging_features.h"
+
+namespace features {
+
+const base::Feature kPushMessagingDisallowSenderIDs{
+ "PushMessagingDisallowSenderIDs", base::FEATURE_DISABLED_BY_DEFAULT};
+
+const base::Feature kPushSubscriptionWithExpirationTime{
+ "PushSubscriptionWithExpirationTime", base::FEATURE_DISABLED_BY_DEFAULT};
+
+} // namespace features
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_features.h b/chromium/chrome/browser/push_messaging/push_messaging_features.h
new file mode 100644
index 00000000000..1bdc9237860
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_features.h
@@ -0,0 +1,21 @@
+// Copyright 2020 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_PUSH_MESSAGING_PUSH_MESSAGING_FEATURES_H_
+#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_FEATURES_H_
+
+#include "base/feature_list.h"
+
+namespace features {
+
+// Feature flag to disallow creation of push messages with GCM Sender IDs.
+extern const base::Feature kPushMessagingDisallowSenderIDs;
+
+// Feature flag to enable push subscription with expiration times specified in
+// /chrome/browser/push_messaging/push_messaging_constants.h
+extern const base::Feature kPushSubscriptionWithExpirationTime;
+
+} // namespace features
+
+#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_FEATURES_H_
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.cc b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.cc
new file mode 100644
index 00000000000..e2a8f2adc42
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.cc
@@ -0,0 +1,353 @@
+// Copyright 2015 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.
+
+#include "chrome/browser/push_messaging/push_messaging_notification_manager.h"
+
+#include <stddef.h>
+
+#include <bitset>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/feature_list.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/task/post_task.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/notifications/platform_notification_service_factory.h"
+#include "chrome/browser/notifications/platform_notification_service_impl.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/push_messaging/push_messaging_constants.h"
+#include "chrome/grit/generated_resources.h"
+#include "components/site_engagement/content/site_engagement_service.h"
+#include "components/url_formatter/elide_url.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/browser_task_traits.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/platform_notification_context.h"
+#include "content/public/browser/push_messaging_service.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/storage_partition.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/common/content_features.h"
+#include "content/public/common/page_visibility_state.h"
+#include "content/public/common/url_constants.h"
+#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
+#include "third_party/blink/public/common/notifications/notification_resources.h"
+#include "third_party/blink/public/mojom/notifications/notification.mojom-shared.h"
+#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "url/gurl.h"
+
+#if defined(OS_ANDROID)
+#include "chrome/browser/ui/android/tab_model/tab_model.h"
+#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
+#else
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/ash/android_sms/android_sms_service_factory.h"
+#include "chrome/browser/ash/android_sms/android_sms_urls.h"
+#include "chrome/browser/ash/multidevice_setup/multidevice_setup_client_factory.h"
+#endif
+
+using content::BrowserThread;
+using content::NotificationDatabaseData;
+using content::PlatformNotificationContext;
+using content::PushMessagingService;
+using content::ServiceWorkerContext;
+using content::WebContents;
+
+namespace {
+void RecordUserVisibleStatus(blink::mojom::PushUserVisibleStatus status) {
+ UMA_HISTOGRAM_ENUMERATION("PushMessaging.UserVisibleStatus", status);
+}
+
+content::StoragePartition* GetStoragePartition(Profile* profile,
+ const GURL& origin) {
+ return profile->GetStoragePartitionForUrl(origin);
+}
+
+NotificationDatabaseData CreateDatabaseData(
+ const GURL& origin,
+ int64_t service_worker_registration_id) {
+ blink::PlatformNotificationData notification_data;
+ notification_data.title = url_formatter::FormatUrlForSecurityDisplay(
+ origin, url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS);
+ notification_data.direction =
+ blink::mojom::NotificationDirection::LEFT_TO_RIGHT;
+ notification_data.body =
+ l10n_util::GetStringUTF16(IDS_PUSH_MESSAGING_GENERIC_NOTIFICATION_BODY);
+ notification_data.tag = kPushMessagingForcedNotificationTag;
+ notification_data.icon = GURL();
+ notification_data.timestamp = base::Time::Now();
+ notification_data.silent = true;
+
+ NotificationDatabaseData database_data;
+ database_data.origin = origin;
+ database_data.service_worker_registration_id = service_worker_registration_id;
+ database_data.notification_data = notification_data;
+
+ // Make sure we don't expose this notification to the site.
+ database_data.is_shown_by_browser = true;
+
+ return database_data;
+}
+
+} // namespace
+
+PushMessagingNotificationManager::PushMessagingNotificationManager(
+ Profile* profile)
+ : profile_(profile), budget_database_(profile) {}
+
+PushMessagingNotificationManager::~PushMessagingNotificationManager() = default;
+
+void PushMessagingNotificationManager::EnforceUserVisibleOnlyRequirements(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ EnforceRequirementsCallback message_handled_callback) {
+ DCHECK_CURRENTLY_ON(BrowserThread::UI);
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ if (ShouldSkipUserVisibleOnlyRequirements(origin)) {
+ std::move(message_handled_callback)
+ .Run(/* did_show_generic_notification= */ false);
+ return;
+ }
+#endif
+
+ // TODO(johnme): Relax this heuristic slightly.
+ scoped_refptr<PlatformNotificationContext> notification_context =
+ GetStoragePartition(profile_, origin)->GetPlatformNotificationContext();
+
+ notification_context->CountVisibleNotificationsForServiceWorkerRegistration(
+ origin, service_worker_registration_id,
+ base::BindOnce(
+ &PushMessagingNotificationManager::DidCountVisibleNotifications,
+ weak_factory_.GetWeakPtr(), origin, service_worker_registration_id,
+ std::move(message_handled_callback)));
+}
+
+void PushMessagingNotificationManager::DidCountVisibleNotifications(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ EnforceRequirementsCallback message_handled_callback,
+ bool success,
+ int notification_count) {
+ DCHECK_CURRENTLY_ON(BrowserThread::UI);
+ // TODO(johnme): Hiding an existing notification should also count as a useful
+ // user-visible action done in response to a push message - but make sure that
+ // sending two messages in rapid succession which show then hide a
+ // notification doesn't count.
+ // TODO(crbug.com/891339): Scheduling a notification should count as a
+ // user-visible action, if it is not immediately cancelled or the |origin|
+ // schedules too many notifications too far in the future.
+ bool notification_shown = notification_count > 0;
+ bool notification_needed = true;
+
+ base::UmaHistogramCounts100("PushMessaging.VisibleNotificationCount",
+ notification_count);
+
+ // Sites with a currently visible tab don't need to show notifications.
+#if defined(OS_ANDROID)
+ for (const TabModel* model : TabModelList::models()) {
+ Profile* profile = model->GetProfile();
+ WebContents* active_web_contents = model->GetActiveWebContents();
+#else
+ for (auto* browser : *BrowserList::GetInstance()) {
+ Profile* profile = browser->profile();
+ WebContents* active_web_contents =
+ browser->tab_strip_model()->GetActiveWebContents();
+#endif
+ if (IsTabVisible(profile, active_web_contents, origin)) {
+ notification_needed = false;
+ break;
+ }
+ }
+
+ // If more than one notification is showing for this Service Worker, close
+ // the default notification if it happens to be part of this group.
+ if (notification_count >= 2) {
+ scoped_refptr<PlatformNotificationContext> notification_context =
+ GetStoragePartition(profile_, origin)->GetPlatformNotificationContext();
+ notification_context->DeleteAllNotificationDataWithTag(
+ kPushMessagingForcedNotificationTag, /*is_shown_by_browser=*/true,
+ origin, base::DoNothing());
+ }
+
+ if (notification_needed && !notification_shown) {
+ // If the worker needed to show a notification and didn't, see if a silent
+ // push was allowed.
+ budget_database_.SpendBudget(
+ url::Origin::Create(origin),
+ base::BindOnce(&PushMessagingNotificationManager::ProcessSilentPush,
+ weak_factory_.GetWeakPtr(), origin,
+ service_worker_registration_id,
+ std::move(message_handled_callback)));
+ return;
+ }
+
+ if (notification_needed && notification_shown) {
+ RecordUserVisibleStatus(
+ blink::mojom::PushUserVisibleStatus::REQUIRED_AND_SHOWN);
+ } else if (!notification_needed && !notification_shown) {
+ RecordUserVisibleStatus(
+ blink::mojom::PushUserVisibleStatus::NOT_REQUIRED_AND_NOT_SHOWN);
+ } else {
+ RecordUserVisibleStatus(
+ blink::mojom::PushUserVisibleStatus::NOT_REQUIRED_BUT_SHOWN);
+ }
+
+ std::move(message_handled_callback)
+ .Run(/* did_show_generic_notification= */ false);
+}
+
+bool PushMessagingNotificationManager::IsTabVisible(
+ Profile* profile,
+ WebContents* active_web_contents,
+ const GURL& origin) {
+ if (!active_web_contents || !active_web_contents->GetMainFrame())
+ return false;
+
+ // Don't leak information from other profiles.
+ if (profile != profile_)
+ return false;
+
+ // Ignore minimized windows.
+ switch (active_web_contents->GetMainFrame()->GetVisibilityState()) {
+ case content::PageVisibilityState::kHidden:
+ case content::PageVisibilityState::kHiddenButPainting:
+ return false;
+ case content::PageVisibilityState::kVisible:
+ break;
+ }
+
+ // Use the visible URL since that's the one the user is aware of (and it
+ // doesn't matter whether the page loaded successfully).
+ GURL visible_url = active_web_contents->GetVisibleURL();
+
+ // view-source: pages are considered to be controlled Service Worker clients
+ // and thus should be considered when checking the visible URL. However, the
+ // prefix has to be removed before the origins can be compared.
+ if (visible_url.SchemeIs(content::kViewSourceScheme))
+ visible_url = GURL(visible_url.GetContent());
+
+ return visible_url.DeprecatedGetOriginAsURL() == origin;
+}
+
+void PushMessagingNotificationManager::ProcessSilentPush(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ EnforceRequirementsCallback message_handled_callback,
+ bool silent_push_allowed) {
+ DCHECK_CURRENTLY_ON(BrowserThread::UI);
+
+ // If the origin was allowed to issue a silent push, just return.
+ if (silent_push_allowed) {
+ RecordUserVisibleStatus(
+ blink::mojom::PushUserVisibleStatus::REQUIRED_BUT_NOT_SHOWN_USED_GRACE);
+ std::move(message_handled_callback)
+ .Run(/* did_show_generic_notification= */ false);
+ return;
+ }
+
+ RecordUserVisibleStatus(blink::mojom::PushUserVisibleStatus::
+ REQUIRED_BUT_NOT_SHOWN_GRACE_EXCEEDED);
+
+ // The site failed to show a notification when one was needed, and they don't
+ // have enough budget to cover the cost of suppressing, so we will show a
+ // generic notification.
+ NotificationDatabaseData database_data =
+ CreateDatabaseData(origin, service_worker_registration_id);
+ scoped_refptr<PlatformNotificationContext> notification_context =
+ GetStoragePartition(profile_, origin)->GetPlatformNotificationContext();
+ int64_t next_persistent_notification_id =
+ PlatformNotificationServiceFactory::GetForProfile(profile_)
+ ->ReadNextPersistentNotificationId();
+
+ notification_context->WriteNotificationData(
+ next_persistent_notification_id, service_worker_registration_id, origin,
+ database_data,
+ base::BindOnce(
+ &PushMessagingNotificationManager::DidWriteNotificationData,
+ weak_factory_.GetWeakPtr(), std::move(message_handled_callback)));
+}
+
+void PushMessagingNotificationManager::DidWriteNotificationData(
+ EnforceRequirementsCallback message_handled_callback,
+ bool success,
+ const std::string& notification_id) {
+ DCHECK_CURRENTLY_ON(BrowserThread::UI);
+ if (!success)
+ DLOG(ERROR) << "Writing forced notification to database should not fail";
+
+ std::move(message_handled_callback)
+ .Run(/* did_show_generic_notification= */ true);
+}
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+bool PushMessagingNotificationManager::ShouldSkipUserVisibleOnlyRequirements(
+ const GURL& origin) {
+ // This is a short-term exception to user visible only enforcement added
+ // to support for "Messages for Web" integration on ChromeOS.
+
+ chromeos::multidevice_setup::MultiDeviceSetupClient* multidevice_setup_client;
+ if (test_multidevice_setup_client_) {
+ multidevice_setup_client = test_multidevice_setup_client_;
+ } else {
+ multidevice_setup_client =
+ ash::multidevice_setup::MultiDeviceSetupClientFactory::GetForProfile(
+ profile_);
+ }
+
+ if (!multidevice_setup_client)
+ return false;
+
+ // Check if messages feature is enabled
+ if (multidevice_setup_client->GetFeatureState(
+ chromeos::multidevice_setup::mojom::Feature::kMessages) !=
+ chromeos::multidevice_setup::mojom::FeatureState::kEnabledByUser) {
+ return false;
+ }
+
+ ash::android_sms::AndroidSmsAppManager* android_sms_app_manager;
+ if (test_android_sms_app_manager_) {
+ android_sms_app_manager = test_android_sms_app_manager_;
+ } else {
+ auto* android_sms_service =
+ ash::android_sms::AndroidSmsServiceFactory::GetForBrowserContext(
+ profile_);
+ if (!android_sms_service)
+ return false;
+ android_sms_app_manager = android_sms_service->android_sms_app_manager();
+ }
+
+ // Check if origin matches current messages url
+ absl::optional<GURL> app_url = android_sms_app_manager->GetCurrentAppUrl();
+ if (!app_url)
+ app_url = ash::android_sms::GetAndroidMessagesURL();
+
+ if (!origin.EqualsIgnoringRef(app_url->DeprecatedGetOriginAsURL()))
+ return false;
+
+ return true;
+}
+
+void PushMessagingNotificationManager::SetTestMultiDeviceSetupClient(
+ chromeos::multidevice_setup::MultiDeviceSetupClient*
+ multidevice_setup_client) {
+ test_multidevice_setup_client_ = multidevice_setup_client;
+}
+
+void PushMessagingNotificationManager::SetTestAndroidSmsAppManager(
+ ash::android_sms::AndroidSmsAppManager* android_sms_app_manager) {
+ test_android_sms_app_manager_ = android_sms_app_manager;
+}
+#endif
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.h b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.h
new file mode 100644
index 00000000000..b2621f55c53
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.h
@@ -0,0 +1,122 @@
+// Copyright 2015 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_PUSH_MESSAGING_PUSH_MESSAGING_NOTIFICATION_MANAGER_H_
+#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_NOTIFICATION_MANAGER_H_
+
+#include <stdint.h>
+#include <vector>
+
+#include "base/callback_forward.h"
+#include "base/gtest_prod_util.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/push_messaging/budget_database.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/ash/android_sms/android_sms_app_manager.h"
+#include "chromeos/services/multidevice_setup/public/cpp/multidevice_setup_client.h"
+#endif
+
+class GURL;
+class Profile;
+
+namespace content {
+class WebContents;
+} // namespace content
+
+// Developers may be required to display a Web Notification in response to an
+// incoming push message in order to clarify to the user that something has
+// happened in the background. When they forget to do so, a default notification
+// has to be displayed on their behalf.
+//
+// This class implements the heuristics for determining whether the default
+// notification is necessary, as well as the functionality of displaying the
+// default notification when it is.
+//
+// See the following document and bug for more context:
+// https://docs.google.com/document/d/13VxFdLJbMwxHrvnpDm8RXnU41W2ZlcP0mdWWe9zXQT8/edit
+// https://crbug.com/437277
+class PushMessagingNotificationManager {
+ public:
+ using EnforceRequirementsCallback =
+ base::OnceCallback<void(bool did_show_generic_notification)>;
+
+ explicit PushMessagingNotificationManager(Profile* profile);
+
+ PushMessagingNotificationManager(const PushMessagingNotificationManager&) =
+ delete;
+ PushMessagingNotificationManager& operator=(
+ const PushMessagingNotificationManager&) = delete;
+
+ ~PushMessagingNotificationManager();
+
+ // Enforces the requirements implied for push subscriptions which must display
+ // a Web Notification in response to an incoming message.
+ void EnforceUserVisibleOnlyRequirements(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ EnforceRequirementsCallback message_handled_callback);
+
+ private:
+ FRIEND_TEST_ALL_PREFIXES(PushMessagingNotificationManagerTest, IsTabVisible);
+ FRIEND_TEST_ALL_PREFIXES(PushMessagingNotificationManagerTest,
+ IsTabVisibleViewSource);
+ FRIEND_TEST_ALL_PREFIXES(
+ PushMessagingNotificationManagerTest,
+ SkipEnforceUserVisibleOnlyRequirementsForAndroidMessages);
+
+ void DidCountVisibleNotifications(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ EnforceRequirementsCallback message_handled_callback,
+ bool success,
+ int notification_count);
+
+ // Checks whether |profile| is the one owning this instance,
+ // |active_web_contents| exists and its main frame is visible, and the URL
+ // currently visible to the user is for |origin|.
+ bool IsTabVisible(Profile* profile,
+ content::WebContents* active_web_contents,
+ const GURL& origin);
+
+ void ProcessSilentPush(const GURL& origin,
+ int64_t service_worker_registration_id,
+ EnforceRequirementsCallback message_handled_callback,
+ bool silent_push_allowed);
+
+ void DidWriteNotificationData(
+ EnforceRequirementsCallback message_handled_callback,
+ bool success,
+ const std::string& notification_id);
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ bool ShouldSkipUserVisibleOnlyRequirements(const GURL& origin);
+
+ void SetTestMultiDeviceSetupClient(
+ chromeos::multidevice_setup::MultiDeviceSetupClient*
+ multidevice_setup_client);
+
+ void SetTestAndroidSmsAppManager(
+ ash::android_sms::AndroidSmsAppManager* android_sms_app_manager);
+#endif
+
+ // Weak. This manager is owned by a keyed service on this profile.
+ raw_ptr<Profile> profile_;
+
+ BudgetDatabase budget_database_;
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ chromeos::multidevice_setup::MultiDeviceSetupClient*
+ test_multidevice_setup_client_ = nullptr;
+
+ ash::android_sms::AndroidSmsAppManager* test_android_sms_app_manager_ =
+ nullptr;
+#endif
+
+ base::WeakPtrFactory<PushMessagingNotificationManager> weak_factory_{this};
+};
+
+#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_NOTIFICATION_MANAGER_H_
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_notification_manager_unittest.cc b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager_unittest.cc
new file mode 100644
index 00000000000..7b6b46680f5
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager_unittest.cc
@@ -0,0 +1,87 @@
+// Copyright 2015 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.
+
+#include "chrome/browser/push_messaging/push_messaging_notification_manager.h"
+
+#include "base/bind.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "chrome/test/base/testing_profile.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/test_renderer_host.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include <memory>
+
+#include "chrome/browser/ash/android_sms/fake_android_sms_app_manager.h"
+#include "chromeos/services/multidevice_setup/public/cpp/fake_multidevice_setup_client.h"
+#endif
+
+class PushMessagingNotificationManagerTest
+ : public ChromeRenderViewHostTestHarness {};
+
+TEST_F(PushMessagingNotificationManagerTest, IsTabVisible) {
+ PushMessagingNotificationManager manager(profile());
+ GURL origin("https://google.com/");
+ GURL origin_with_path = origin.Resolve("/path/");
+ NavigateAndCommit(origin_with_path);
+
+ EXPECT_FALSE(manager.IsTabVisible(profile(), nullptr, origin));
+ EXPECT_FALSE(manager.IsTabVisible(profile(), web_contents(),
+ GURL("https://chrome.com/")));
+ EXPECT_TRUE(manager.IsTabVisible(profile(), web_contents(), origin));
+
+ content::RenderViewHostTester::For(rvh())->SimulateWasHidden();
+ EXPECT_FALSE(manager.IsTabVisible(profile(), web_contents(), origin));
+
+ content::RenderViewHostTester::For(rvh())->SimulateWasShown();
+ EXPECT_TRUE(manager.IsTabVisible(profile(), web_contents(), origin));
+}
+
+TEST_F(PushMessagingNotificationManagerTest, IsTabVisibleViewSource) {
+ PushMessagingNotificationManager manager(profile());
+
+ GURL origin("https://google.com/");
+ GURL view_source_page("view-source:https://google.com/path/");
+
+ NavigateAndCommit(view_source_page);
+
+ ASSERT_EQ(view_source_page, web_contents()->GetVisibleURL());
+ EXPECT_TRUE(manager.IsTabVisible(profile(), web_contents(), origin));
+
+ content::RenderViewHostTester::For(rvh())->SimulateWasHidden();
+ EXPECT_FALSE(manager.IsTabVisible(profile(), web_contents(), origin));
+}
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+TEST_F(PushMessagingNotificationManagerTest,
+ SkipEnforceUserVisibleOnlyRequirementsForAndroidMessages) {
+ GURL app_url("https://example.com/test/");
+ auto fake_android_sms_app_manager =
+ std::make_unique<ash::android_sms::FakeAndroidSmsAppManager>();
+ fake_android_sms_app_manager->SetInstalledAppUrl(app_url);
+
+ auto fake_multidevice_setup_client = std::make_unique<
+ chromeos::multidevice_setup::FakeMultiDeviceSetupClient>();
+ fake_multidevice_setup_client->SetFeatureState(
+ chromeos::multidevice_setup::mojom::Feature::kMessages,
+ chromeos::multidevice_setup::mojom::FeatureState::kEnabledByUser);
+
+ PushMessagingNotificationManager manager(profile());
+ manager.SetTestMultiDeviceSetupClient(fake_multidevice_setup_client.get());
+ manager.SetTestAndroidSmsAppManager(fake_android_sms_app_manager.get());
+
+ bool was_called = false;
+ manager.EnforceUserVisibleOnlyRequirements(
+ app_url.DeprecatedGetOriginAsURL(), 0l,
+ base::BindOnce(
+ [](bool* was_called, bool did_show_generic_notification) {
+ *was_called = true;
+ },
+ &was_called));
+ EXPECT_TRUE(was_called);
+}
+#endif
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_refresher.cc b/chromium/chrome/browser/push_messaging/push_messaging_refresher.cc
new file mode 100644
index 00000000000..2f8bdee4b43
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_refresher.cc
@@ -0,0 +1,121 @@
+// Copyright 2020 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.
+
+#include "chrome/browser/push_messaging/push_messaging_refresher.h"
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/feature_list.h"
+#include "chrome/browser/push_messaging/push_messaging_app_identifier.h"
+#include "chrome/browser/push_messaging/push_messaging_constants.h"
+#include "chrome/browser/push_messaging/push_messaging_service_impl.h"
+#include "chrome/browser/push_messaging/push_messaging_utils.h"
+#include "chrome/common/pref_names.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/push_messaging_service.h"
+#include "content/public/common/content_features.h"
+#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h"
+#include "url/gurl.h"
+
+PushMessagingRefresher::PushMessagingRefresher() = default;
+
+PushMessagingRefresher::~PushMessagingRefresher() = default;
+
+size_t PushMessagingRefresher::GetCount() const {
+ return old_subscriptions_.size();
+}
+
+void PushMessagingRefresher::Refresh(
+ PushMessagingAppIdentifier old_app_identifier,
+ const std::string& new_app_id,
+ const std::string& sender_id) {
+ RefreshObject refresh_object = {old_app_identifier, sender_id,
+ false /* is_valid */};
+ // Insert is as current started refresh
+ old_subscriptions_.emplace(new_app_id, refresh_object);
+ refresh_map_.emplace(old_app_identifier.app_id(), new_app_id);
+ // TODO(viviy): Save old_subscription in a seperate map in preferences, so
+ // that in case of a browser shutdown the subscription is remembered.
+ // Unsubscribe on next startup.
+}
+
+void PushMessagingRefresher::OnSubscriptionUpdated(
+ const std::string& new_app_id) {
+ RefreshInfo::iterator result = old_subscriptions_.find(new_app_id);
+
+ if (result == old_subscriptions_.end())
+ return;
+
+ // Schedule a unsubscription event for the old subscription
+ base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
+ FROM_HERE,
+ base::BindOnce(&PushMessagingRefresher::NotifyOnOldSubscriptionExpired,
+ weak_factory_.GetWeakPtr(),
+ result->second.old_identifier.app_id(),
+ result->second.sender_id),
+ kPushSubscriptionRefreshTimeDelta);
+}
+
+void PushMessagingRefresher::NotifyOnOldSubscriptionExpired(
+ const std::string& old_app_id,
+ const std::string& sender_id) {
+ for (Observer& obs : observers_)
+ obs.OnOldSubscriptionExpired(old_app_id, sender_id);
+}
+
+void PushMessagingRefresher::OnUnsubscribed(const std::string& old_app_id) {
+ auto found_new_app_id = refresh_map_.find(old_app_id);
+ // Already unsubscribed
+ if (found_new_app_id == refresh_map_.end())
+ return;
+
+ std::string new_app_id = found_new_app_id->second;
+ refresh_map_.erase(found_new_app_id);
+
+ RefreshInfo::iterator result = old_subscriptions_.find(new_app_id);
+ DCHECK(result != old_subscriptions_.end());
+
+ PushMessagingAppIdentifier old_identifier = result->second.old_identifier;
+ old_subscriptions_.erase(result);
+
+ for (Observer& obs : observers_)
+ obs.OnRefreshFinished(old_identifier);
+}
+
+void PushMessagingRefresher::GotMessageFrom(const std::string& app_id) {
+ RefreshInfo::iterator result = old_subscriptions_.find(app_id);
+ // If a message arrives that is part of the refresh, expire the old
+ // subscription immediately
+ if (result != old_subscriptions_.end() && !result->second.is_valid) {
+ NotifyOnOldSubscriptionExpired(result->second.old_identifier.app_id(),
+ result->second.sender_id);
+ result->second.is_valid = true;
+ }
+}
+
+absl::optional<PushMessagingAppIdentifier>
+PushMessagingRefresher::FindActiveAppIdentifier(const std::string& app_id) {
+ absl::optional<PushMessagingAppIdentifier> app_identifier;
+ RefreshMap::iterator refresh_map_it = refresh_map_.find(app_id);
+ if (refresh_map_it != refresh_map_.end()) {
+ RefreshInfo::iterator result =
+ old_subscriptions_.find(refresh_map_it->second);
+ if (result != old_subscriptions_.end() && !result->second.is_valid) {
+ app_identifier = result->second.old_identifier;
+ }
+ }
+ return app_identifier;
+}
+
+base::WeakPtr<PushMessagingRefresher> PushMessagingRefresher::GetWeakPtr() {
+ return weak_factory_.GetWeakPtr();
+}
+
+void PushMessagingRefresher::AddObserver(Observer* observer) {
+ observers_.AddObserver(observer);
+}
+
+void PushMessagingRefresher::RemoveObserver(Observer* observer) {
+ observers_.RemoveObserver(observer);
+}
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_refresher.h b/chromium/chrome/browser/push_messaging/push_messaging_refresher.h
new file mode 100644
index 00000000000..c049a14388e
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_refresher.h
@@ -0,0 +1,99 @@
+// Copyright 2020 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_PUSH_MESSAGING_PUSH_MESSAGING_REFRESHER_H_
+#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_REFRESHER_H_
+
+#include <map>
+#include <vector>
+
+#include "base/memory/weak_ptr.h"
+#include "base/observer_list.h"
+#include "base/observer_list_types.h"
+#include "chrome/browser/push_messaging/push_messaging_app_identifier.h"
+#include "content/public/browser/push_messaging_service.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/blink/public/mojom/push_messaging/push_messaging.mojom-forward.h"
+
+// This class enables push subscription refreshes as defined in the docs:
+// https://w3c.github.io/push-api/#subscription-refreshes
+// The idea is to keep the refresh information of both new and old subscription
+// in memory during the refresh process to be still able to receive messages
+// through the old subscription after it was replaced by the new subscription.
+class PushMessagingRefresher {
+ public:
+ PushMessagingRefresher();
+
+ PushMessagingRefresher(const PushMessagingRefresher&) = delete;
+ PushMessagingRefresher& operator=(const PushMessagingRefresher&) = delete;
+
+ ~PushMessagingRefresher();
+
+ // Return number of objects that are currently being refreshed
+ size_t GetCount() const;
+
+ // Register a new refresh pair with relevant information.
+ void Refresh(PushMessagingAppIdentifier old_app_identifier,
+ const std::string& new_app_id,
+ const std::string& sender_id);
+
+ // The subscription with the new app id was updated, new messages arriving
+ // through the new subscription should be accepted now.
+ void OnSubscriptionUpdated(const std::string& new_app_id);
+
+ // Unsubscribe event happened for the old subscription. It is deleted in
+ // the RefreshMap and notify all observers that the refresh process for
+ // |app_id| has finished
+ void OnUnsubscribed(const std::string& app_id);
+
+ // If a new message arrives through an |app_id| that is associated with a
+ // refresh, the old subscription needs to be deactivated.
+ void GotMessageFrom(const std::string& app_id);
+
+ // If a subscription was refreshed, we accept the old subscription for
+ // a moment after refresh
+ absl::optional<PushMessagingAppIdentifier> FindActiveAppIdentifier(
+ const std::string& app_id);
+
+ base::WeakPtr<PushMessagingRefresher> GetWeakPtr();
+
+ // Observer for Refresh status updates
+ class Observer : public base::CheckedObserver {
+ public:
+ virtual void OnOldSubscriptionExpired(const std::string& app_id,
+ const std::string& sender_id) = 0;
+ virtual void OnRefreshFinished(
+ const PushMessagingAppIdentifier& app_identifier) = 0;
+ };
+
+ void AddObserver(Observer* observer);
+ void RemoveObserver(Observer* observer);
+
+ private:
+ // A RefreshObject carries subscription information that is needed to receive
+ // messages and to unsubscribe from the old subscription
+ struct RefreshObject {
+ PushMessagingAppIdentifier old_identifier;
+ std::string sender_id;
+ bool is_valid;
+ };
+
+ void NotifyOnOldSubscriptionExpired(const std::string& app_id,
+ const std::string& sender_id);
+
+ base::ObserverList<Observer> observers_;
+
+ // Maps from new app id to the refresh information of the old subscription
+ // that is needed to receive messages and unsubscribe
+ using RefreshInfo = std::map<std::string, RefreshObject>;
+ RefreshInfo old_subscriptions_;
+
+ // Maps from old app id to new app id
+ using RefreshMap = std::map<std::string, std::string>;
+ RefreshMap refresh_map_;
+
+ base::WeakPtrFactory<PushMessagingRefresher> weak_factory_{this};
+};
+
+#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_REFRESHER_H_
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_refresher_unittest.cc b/chromium/chrome/browser/push_messaging/push_messaging_refresher_unittest.cc
new file mode 100644
index 00000000000..63570f41e83
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_refresher_unittest.cc
@@ -0,0 +1,84 @@
+// Copyright 2020 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.
+
+#include "chrome/browser/push_messaging/push_messaging_refresher.h"
+
+#include <stdint.h>
+
+#include "base/time/time.h"
+#include "chrome/browser/push_messaging/push_messaging_app_identifier.h"
+#include "chrome/browser/push_messaging/push_messaging_refresher.h"
+#include "chrome/test/base/testing_profile.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "url/gurl.h"
+namespace {
+
+void ExpectAppIdentifiersEqual(const PushMessagingAppIdentifier& a,
+ const PushMessagingAppIdentifier& b) {
+ EXPECT_EQ(a.app_id(), b.app_id());
+ EXPECT_EQ(a.origin(), b.origin());
+ EXPECT_EQ(a.service_worker_registration_id(),
+ b.service_worker_registration_id());
+ EXPECT_EQ(a.expiration_time(), b.expiration_time());
+}
+
+constexpr char kTestOrigin[] = "https://example.com";
+constexpr char kTestSenderId[] = "1234567890";
+const int64_t kTestServiceWorkerId = 42;
+
+class PushMessagingRefresherTest : public testing::Test {
+ protected:
+ void SetUp() override {
+ old_app_identifier_ = PushMessagingAppIdentifier::Generate(
+ GURL(kTestOrigin), kTestServiceWorkerId);
+ new_app_identifier_ = PushMessagingAppIdentifier::Generate(
+ GURL(kTestOrigin), kTestServiceWorkerId);
+ }
+
+ Profile* profile() { return &profile_; }
+
+ PushMessagingRefresher* refresher() { return &refresher_; }
+
+ absl::optional<PushMessagingAppIdentifier> old_app_identifier_;
+ absl::optional<PushMessagingAppIdentifier> new_app_identifier_;
+
+ private:
+ content::BrowserTaskEnvironment task_environment_;
+ TestingProfile profile_;
+ PushMessagingRefresher refresher_;
+};
+
+TEST_F(PushMessagingRefresherTest, GotMessageThroughNewSubscription) {
+ refresher()->Refresh(old_app_identifier_.value(),
+ new_app_identifier_.value().app_id(), kTestSenderId);
+ refresher()->GotMessageFrom(new_app_identifier_.value().app_id());
+ auto app_identifier = refresher()->FindActiveAppIdentifier(
+ old_app_identifier_.value().app_id());
+ EXPECT_FALSE(app_identifier.has_value());
+}
+
+TEST_F(PushMessagingRefresherTest, LookupOldSubscription) {
+ refresher()->Refresh(old_app_identifier_.value(),
+ new_app_identifier_.value().app_id(), kTestSenderId);
+ {
+ absl::optional<PushMessagingAppIdentifier> found_old_app_identifier =
+ refresher()->FindActiveAppIdentifier(
+ old_app_identifier_.value().app_id());
+ EXPECT_TRUE(found_old_app_identifier.has_value());
+ ExpectAppIdentifiersEqual(old_app_identifier_.value(),
+ found_old_app_identifier.value());
+ }
+ refresher()->OnUnsubscribed(old_app_identifier_.value().app_id());
+ {
+ absl::optional<PushMessagingAppIdentifier> found_after_unsubscribe =
+ refresher()->FindActiveAppIdentifier(
+ old_app_identifier_.value().app_id());
+ EXPECT_FALSE(found_after_unsubscribe.has_value());
+ }
+ EXPECT_EQ(0u, refresher()->GetCount());
+}
+
+} // namespace
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_factory.cc b/chromium/chrome/browser/push_messaging/push_messaging_service_factory.cc
new file mode 100644
index 00000000000..2987c24efdf
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_service_factory.cc
@@ -0,0 +1,82 @@
+// Copyright 2015 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.
+
+#include "chrome/browser/push_messaging/push_messaging_service_factory.h"
+
+#include <memory>
+
+#include "base/bind.h"
+#include "base/memory/ptr_util.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
+#include "chrome/browser/engagement/site_engagement_service_factory.h"
+#include "chrome/browser/gcm/gcm_profile_service_factory.h"
+#include "chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h"
+#include "chrome/browser/permissions/permission_manager_factory.h"
+#include "chrome/browser/profiles/incognito_helpers.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/push_messaging/push_messaging_service_impl.h"
+#include "components/gcm_driver/instance_id/instance_id_profile_service.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/ash/android_sms/android_sms_service_factory.h"
+#include "chrome/browser/ash/multidevice_setup/multidevice_setup_client_factory.h"
+#endif
+
+// static
+PushMessagingServiceImpl* PushMessagingServiceFactory::GetForProfile(
+ content::BrowserContext* context) {
+ // The Push API is not currently supported in incognito mode.
+ // See https://crbug.com/401439.
+ if (context->IsOffTheRecord())
+ return nullptr;
+
+ return static_cast<PushMessagingServiceImpl*>(
+ GetInstance()->GetServiceForBrowserContext(context, true));
+}
+
+// static
+PushMessagingServiceFactory* PushMessagingServiceFactory::GetInstance() {
+ return base::Singleton<PushMessagingServiceFactory>::get();
+}
+
+PushMessagingServiceFactory::PushMessagingServiceFactory()
+ : BrowserContextKeyedServiceFactory(
+ "PushMessagingProfileService",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(gcm::GCMProfileServiceFactory::GetInstance());
+ DependsOn(instance_id::InstanceIDProfileServiceFactory::GetInstance());
+ DependsOn(HostContentSettingsMapFactory::GetInstance());
+ DependsOn(PermissionManagerFactory::GetInstance());
+ DependsOn(site_engagement::SiteEngagementServiceFactory::GetInstance());
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ DependsOn(ash::android_sms::AndroidSmsServiceFactory::GetInstance());
+ DependsOn(
+ ash::multidevice_setup::MultiDeviceSetupClientFactory::GetInstance());
+#endif
+}
+
+PushMessagingServiceFactory::~PushMessagingServiceFactory() {}
+
+void PushMessagingServiceFactory::RestoreFactoryForTests(
+ content::BrowserContext* context) {
+ SetTestingFactory(context,
+ base::BindRepeating([](content::BrowserContext* context) {
+ return base::WrapUnique(
+ GetInstance()->BuildServiceInstanceFor(context));
+ }));
+}
+
+KeyedService* PushMessagingServiceFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ CHECK(!profile->IsOffTheRecord());
+ return new PushMessagingServiceImpl(profile);
+}
+
+content::BrowserContext* PushMessagingServiceFactory::GetBrowserContextToUse(
+ content::BrowserContext* context) const {
+ return chrome::GetBrowserContextOwnInstanceInIncognito(context);
+}
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_factory.h b/chromium/chrome/browser/push_messaging/push_messaging_service_factory.h
new file mode 100644
index 00000000000..5512ed8dfe7
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_service_factory.h
@@ -0,0 +1,40 @@
+// Copyright 2015 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_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_FACTORY_H_
+#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class PushMessagingServiceImpl;
+
+class PushMessagingServiceFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ static PushMessagingServiceImpl* GetForProfile(
+ content::BrowserContext* profile);
+ static PushMessagingServiceFactory* GetInstance();
+
+ PushMessagingServiceFactory(const PushMessagingServiceFactory&) = delete;
+ PushMessagingServiceFactory& operator=(const PushMessagingServiceFactory&) =
+ delete;
+
+ // Undo a previous call to SetTestingFactory, such that subsequent calls to
+ // GetForProfile get a real push service.
+ void RestoreFactoryForTests(content::BrowserContext* context);
+
+ private:
+ friend struct base::DefaultSingletonTraits<PushMessagingServiceFactory>;
+
+ PushMessagingServiceFactory();
+ ~PushMessagingServiceFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+ content::BrowserContext* GetBrowserContextToUse(
+ content::BrowserContext* context) const override;
+};
+
+#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_FACTORY_H_
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_impl.cc b/chromium/chrome/browser/push_messaging/push_messaging_service_impl.cc
new file mode 100644
index 00000000000..6313d399760
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_service_impl.cc
@@ -0,0 +1,1684 @@
+// Copyright 2014 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.
+
+#include "chrome/browser/push_messaging/push_messaging_service_impl.h"
+
+#include <map>
+#include <sstream>
+#include <vector>
+
+#include "base/barrier_closure.h"
+#include "base/base64url.h"
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/command_line.h"
+#include "base/feature_list.h"
+#include "base/logging.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/strings/string_util.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/time/time.h"
+#include "build/build_config.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/chrome_notification_types.h"
+#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
+#include "chrome/browser/gcm/gcm_profile_service_factory.h"
+#include "chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h"
+#include "chrome/browser/permissions/abusive_origin_permission_revocation_request.h"
+#include "chrome/browser/permissions/permission_manager_factory.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_keep_alive_types.h"
+#include "chrome/browser/profiles/scoped_profile_keep_alive.h"
+#include "chrome/browser/push_messaging/push_messaging_app_identifier.h"
+#include "chrome/browser/push_messaging/push_messaging_constants.h"
+#include "chrome/browser/push_messaging/push_messaging_features.h"
+#include "chrome/browser/push_messaging/push_messaging_service_factory.h"
+#include "chrome/browser/push_messaging/push_messaging_utils.h"
+#include "chrome/browser/ui/chrome_pages.h"
+#include "chrome/common/buildflags.h"
+#include "chrome/common/chrome_features.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/grit/generated_resources.h"
+#include "components/content_settings/core/browser/host_content_settings_map.h"
+#include "components/gcm_driver/gcm_driver.h"
+#include "components/gcm_driver/gcm_profile_service.h"
+#include "components/gcm_driver/instance_id/instance_id.h"
+#include "components/gcm_driver/instance_id/instance_id_driver.h"
+#include "components/gcm_driver/instance_id/instance_id_profile_service.h"
+#include "components/permissions/permission_manager.h"
+#include "components/permissions/permission_result.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/prefs/pref_service.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/devtools_background_services_context.h"
+#include "content/public/browser/notification_service.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/service_worker_context.h"
+#include "content/public/browser/storage_partition.h"
+#include "content/public/common/child_process_host.h"
+#include "content/public/common/content_features.h"
+#include "content/public/common/content_switches.h"
+#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
+#include "third_party/blink/public/mojom/permissions/permission_status.mojom.h"
+#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h"
+#include "ui/base/l10n/l10n_util.h"
+
+#if BUILDFLAG(ENABLE_BACKGROUND_MODE)
+#include "chrome/browser/background/background_mode_manager.h"
+#include "components/keep_alive_registry/keep_alive_types.h"
+#include "components/keep_alive_registry/scoped_keep_alive.h"
+#endif
+
+#if defined(OS_ANDROID)
+#include "base/android/jni_android.h"
+#include "chrome/android/chrome_jni_headers/PushMessagingServiceObserver_jni.h"
+#endif
+
+using instance_id::InstanceID;
+
+namespace {
+
+// Scope passed to getToken to obtain GCM registration tokens.
+// Must match Java GoogleCloudMessaging.INSTANCE_ID_SCOPE.
+const char kGCMScope[] = "GCM";
+
+const int kMaxRegistrations = 1000000;
+
+// Chrome does not yet support silent push messages, and requires websites to
+// indicate that they will only send user-visible messages.
+const char kSilentPushUnsupportedMessage[] =
+ "Chrome currently only supports the Push API for subscriptions that will "
+ "result in user-visible messages. You can indicate this by calling "
+ "pushManager.subscribe({userVisibleOnly: true}) instead. See "
+ "https://goo.gl/yqv4Q4 for more details.";
+
+// Message displayed in the console (as an error) when a GCM Sender ID is used
+// to create a subscription, which is unsupported. The subscription request will
+// have been blocked, and an exception will be thrown as well.
+const char kSenderIdRegistrationDisallowedMessage[] =
+ "The provided application server key is not a VAPID key. Only VAPID keys "
+ "are supported. For more information check https://crbug.com/979235.";
+
+// Message displayed in the console (as a warning) when a GCM Sender ID is used
+// to create a subscription, which will soon be unsupported.
+const char kSenderIdRegistrationDeprecatedMessage[] =
+ "The provided application server key is not a VAPID key. Only VAPID keys "
+ "will be supported in the future. For more information check "
+ "https://crbug.com/979235.";
+
+void RecordDeliveryStatus(blink::mojom::PushEventStatus status) {
+ UMA_HISTOGRAM_ENUMERATION("PushMessaging.DeliveryStatus", status);
+}
+
+void RecordPushSubcriptionChangeStatus(blink::mojom::PushEventStatus status) {
+ UMA_HISTOGRAM_ENUMERATION("PushMessaging.PushSubscriptionChangeStatus",
+ status);
+}
+void RecordUnsubscribeReason(blink::mojom::PushUnregistrationReason reason) {
+ UMA_HISTOGRAM_ENUMERATION("PushMessaging.UnregistrationReason", reason);
+}
+
+void RecordUnsubscribeGCMResult(gcm::GCMClient::Result result) {
+ UMA_HISTOGRAM_ENUMERATION("PushMessaging.UnregistrationGCMResult", result);
+}
+
+void RecordUnsubscribeIIDResult(InstanceID::Result result) {
+ UMA_HISTOGRAM_ENUMERATION("PushMessaging.UnregistrationIIDResult", result);
+}
+
+blink::mojom::PermissionStatus ToPermissionStatus(
+ ContentSetting content_setting) {
+ switch (content_setting) {
+ case CONTENT_SETTING_ALLOW:
+ return blink::mojom::PermissionStatus::GRANTED;
+ case CONTENT_SETTING_BLOCK:
+ return blink::mojom::PermissionStatus::DENIED;
+ case CONTENT_SETTING_ASK:
+ return blink::mojom::PermissionStatus::ASK;
+ default:
+ break;
+ }
+ NOTREACHED();
+ return blink::mojom::PermissionStatus::DENIED;
+}
+
+void UnregisterCallbackToClosure(
+ base::OnceClosure closure,
+ blink::mojom::PushUnregistrationStatus status) {
+ DCHECK(closure);
+ std::move(closure).Run();
+}
+
+void LogMessageReceivedEventToDevTools(
+ content::DevToolsBackgroundServicesContext* devtools_context,
+ const PushMessagingAppIdentifier& app_identifier,
+ const std::string& message_id,
+ bool was_encrypted,
+ const std::string& error_message,
+ const std::string& payload) {
+ if (!devtools_context)
+ return;
+
+ std::map<std::string, std::string> event_metadata = {
+ {"Success", error_message.empty() ? "Yes" : "No"},
+ {"Was Encrypted", was_encrypted ? "Yes" : "No"}};
+
+ if (!error_message.empty())
+ event_metadata["Error Reason"] = error_message;
+ else if (was_encrypted)
+ event_metadata["Payload"] = payload;
+
+ devtools_context->LogBackgroundServiceEvent(
+ app_identifier.service_worker_registration_id(),
+ url::Origin::Create(app_identifier.origin()),
+ content::DevToolsBackgroundService::kPushMessaging,
+ "Push message received" /* event_name */, message_id, event_metadata);
+}
+
+PendingMessage::PendingMessage(std::string app_id, gcm::IncomingMessage message)
+ : app_id(std::move(app_id)),
+ message(std::move(message)),
+ received_time(base::Time::Now()) {}
+PendingMessage::PendingMessage(const PendingMessage& other) = default;
+PendingMessage::PendingMessage(PendingMessage&& other) = default;
+PendingMessage& PendingMessage::operator=(PendingMessage&& other) = default;
+PendingMessage::~PendingMessage() = default;
+
+} // namespace
+
+// static
+void PushMessagingServiceImpl::InitializeForProfile(Profile* profile) {
+ // TODO(johnme): Consider whether push should be enabled in incognito.
+ if (!profile || profile->IsOffTheRecord())
+ return;
+
+ int count = PushMessagingAppIdentifier::GetCount(profile);
+ if (count <= 0)
+ return;
+
+ PushMessagingServiceImpl* push_service =
+ PushMessagingServiceFactory::GetForProfile(profile);
+ if (push_service) {
+ push_service->IncreasePushSubscriptionCount(count, false /* is_pending */);
+ push_service->RemoveExpiredSubscriptions();
+ }
+}
+
+void PushMessagingServiceImpl::RemoveExpiredSubscriptions() {
+ if (!base::FeatureList::IsEnabled(
+ features::kPushSubscriptionWithExpirationTime)) {
+ return;
+ }
+
+ base::RepeatingClosure barrier_closure = base::BarrierClosure(
+ PushMessagingAppIdentifier::GetCount(profile_),
+ remove_expired_subscriptions_callback_for_testing_.is_null()
+ ? base::DoNothing()
+ : std::move(remove_expired_subscriptions_callback_for_testing_));
+
+ for (const auto& identifier : PushMessagingAppIdentifier::GetAll(profile_)) {
+ if (!identifier.IsExpired()) {
+ base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, barrier_closure);
+ continue;
+ }
+ content::BrowserThread::PostBestEffortTask(
+ FROM_HERE, base::ThreadTaskRunnerHandle::Get(),
+ base::BindOnce(
+ &PushMessagingServiceImpl::UnexpectedChange,
+ weak_factory_.GetWeakPtr(), identifier,
+ blink::mojom::PushUnregistrationReason::SUBSCRIPTION_EXPIRED,
+ barrier_closure));
+ }
+}
+
+void PushMessagingServiceImpl::UnexpectedChange(
+ PushMessagingAppIdentifier identifier,
+ blink::mojom::PushUnregistrationReason reason,
+ base::OnceClosure completed_closure) {
+ auto unsubscribe_closure =
+ base::BindOnce(&PushMessagingServiceImpl::UnexpectedUnsubscribe,
+ weak_factory_.GetWeakPtr(), identifier, reason,
+ base::BindOnce(&UnregisterCallbackToClosure,
+ std::move(completed_closure)));
+ if (base::FeatureList::IsEnabled(features::kPushSubscriptionChangeEvent)) {
+ // Find old subscription and fire a `pushsubscriptionchange` event
+ GetPushSubscriptionFromAppIdentifier(
+ identifier,
+ base::BindOnce(&PushMessagingServiceImpl::FirePushSubscriptionChange,
+ weak_factory_.GetWeakPtr(), identifier,
+ std::move(unsubscribe_closure),
+ nullptr /* new_subscription */));
+ } else {
+ std::move(unsubscribe_closure).Run();
+ }
+}
+
+PushMessagingServiceImpl::PushMessagingServiceImpl(Profile* profile)
+ : profile_(profile),
+ push_subscription_count_(0),
+ pending_push_subscription_count_(0),
+ notification_manager_(profile) {
+ DCHECK(profile);
+ HostContentSettingsMapFactory::GetForProfile(profile_)->AddObserver(this);
+
+ registrar_.Add(this, chrome::NOTIFICATION_APP_TERMINATING,
+ content::NotificationService::AllSources());
+ refresh_observation_.Observe(&refresher_);
+}
+
+PushMessagingServiceImpl::~PushMessagingServiceImpl() = default;
+
+void PushMessagingServiceImpl::IncreasePushSubscriptionCount(int add,
+ bool is_pending) {
+ DCHECK_GT(add, 0);
+ if (push_subscription_count_ + pending_push_subscription_count_ == 0)
+ GetGCMDriver()->AddAppHandler(kPushMessagingAppIdentifierPrefix, this);
+
+ if (is_pending)
+ pending_push_subscription_count_ += add;
+ else
+ push_subscription_count_ += add;
+}
+
+void PushMessagingServiceImpl::DecreasePushSubscriptionCount(int subtract,
+ bool was_pending) {
+ DCHECK_GT(subtract, 0);
+ if (was_pending) {
+ pending_push_subscription_count_ -= subtract;
+ DCHECK_GE(pending_push_subscription_count_, 0);
+ } else {
+ push_subscription_count_ -= subtract;
+ DCHECK_GE(push_subscription_count_, 0);
+ }
+
+ if (push_subscription_count_ + pending_push_subscription_count_ == 0)
+ GetGCMDriver()->RemoveAppHandler(kPushMessagingAppIdentifierPrefix);
+}
+
+bool PushMessagingServiceImpl::CanHandle(const std::string& app_id) const {
+ return base::StartsWith(app_id, kPushMessagingAppIdentifierPrefix,
+ base::CompareCase::INSENSITIVE_ASCII);
+}
+
+void PushMessagingServiceImpl::ShutdownHandler() {
+ // Shutdown() should come before and it removes us from the list of app
+ // handlers of gcm::GCMDriver so this shouldn't ever been called.
+ NOTREACHED();
+}
+
+void PushMessagingServiceImpl::OnStoreReset() {
+ // Delete all cached subscriptions, since they are now invalid.
+ for (const auto& identifier : PushMessagingAppIdentifier::GetAll(profile_)) {
+ RecordUnsubscribeReason(
+ blink::mojom::PushUnregistrationReason::GCM_STORE_RESET);
+ // Clear all the subscriptions in parallel, to reduce risk that shutdown
+ // occurs before we finish clearing them.
+ ClearPushSubscriptionId(profile_, identifier.origin(),
+ identifier.service_worker_registration_id(),
+ base::DoNothing());
+ // TODO(johnme): Fire pushsubscriptionchange/pushsubscriptionlost SW event.
+ }
+ PushMessagingAppIdentifier::DeleteAllFromPrefs(profile_);
+}
+
+// OnMessage methods -----------------------------------------------------------
+
+void PushMessagingServiceImpl::OnMessage(const std::string& app_id,
+ const gcm::IncomingMessage& message) {
+ // We won't have time to process and act on the message.
+ // TODO(peter) This should be checked at the level of the GCMDriver, so that
+ // the message is not consumed. See https://crbug.com/612815
+ if (g_browser_process->IsShuttingDown() || shutdown_started_)
+ return;
+
+#if BUILDFLAG(ENABLE_BACKGROUND_MODE)
+ if (g_browser_process->background_mode_manager()) {
+ UMA_HISTOGRAM_BOOLEAN("PushMessaging.ReceivedMessageInBackground",
+ g_browser_process->background_mode_manager()
+ ->IsBackgroundWithoutWindows());
+ }
+
+ if (!in_flight_keep_alive_) {
+ in_flight_keep_alive_ = std::make_unique<ScopedKeepAlive>(
+ KeepAliveOrigin::IN_FLIGHT_PUSH_MESSAGE,
+ KeepAliveRestartOption::DISABLED);
+ in_flight_profile_keep_alive_ = std::make_unique<ScopedProfileKeepAlive>(
+ profile_, ProfileKeepAliveOrigin::kInFlightPushMessage);
+ }
+#endif
+
+ refresher_.GotMessageFrom(app_id);
+
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile_, app_id);
+ // Drop message and unregister if app_id was unknown (maybe recently deleted).
+ if (app_identifier.is_null()) {
+ absl::optional<PushMessagingAppIdentifier> refresh_identifier =
+ refresher_.FindActiveAppIdentifier(app_id);
+ if (!refresh_identifier) {
+ DeliverMessageCallback(app_id, GURL::EmptyGURL(),
+ -1 /* kInvalidServiceWorkerRegistrationId */,
+ message, false /* did_enqueue_message */,
+ blink::mojom::PushEventStatus::UNKNOWN_APP_ID);
+ return;
+ }
+ app_identifier = std::move(*refresh_identifier);
+ }
+
+ LogMessageReceivedEventToDevTools(
+ GetDevToolsContext(app_identifier.origin()), app_identifier,
+ message.message_id,
+ /* was_encrypted= */ message.decrypted, std::string() /* error_message */,
+ message.decrypted ? message.raw_data : std::string());
+
+ if (IsPermissionSet(app_identifier.origin())) {
+ messages_pending_permission_check_.emplace(app_id, message);
+ // Start abusive origin verification only if no other verification is in
+ // progress.
+ if (!abusive_origin_revocation_request_)
+ CheckOriginForAbuseAndDispatchNextMessage();
+ } else {
+ // Drop message and unregister if origin has lost push permission.
+ DeliverMessageCallback(app_id, app_identifier.origin(),
+ app_identifier.service_worker_registration_id(),
+ message, false /* did_enqueue_message */,
+ blink::mojom::PushEventStatus::PERMISSION_DENIED);
+ }
+}
+
+void PushMessagingServiceImpl::CheckOriginForAbuseAndDispatchNextMessage() {
+ if (messages_pending_permission_check_.empty())
+ return;
+
+ PendingMessage message =
+ std::move(messages_pending_permission_check_.front());
+ messages_pending_permission_check_.pop();
+
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile_, message.app_id);
+
+ if (app_identifier.is_null()) {
+ CheckOriginForAbuseAndDispatchNextMessage();
+ return;
+ }
+
+ DCHECK(!abusive_origin_revocation_request_)
+ << "Create one Abusive Origin Revocation instance per request.";
+ abusive_origin_revocation_request_ =
+ std::make_unique<AbusiveOriginPermissionRevocationRequest>(
+ profile_, app_identifier.origin(),
+ base::BindOnce(&PushMessagingServiceImpl::OnCheckedOriginForAbuse,
+ weak_factory_.GetWeakPtr(), std::move(message)));
+}
+
+void PushMessagingServiceImpl::OnCheckedOriginForAbuse(
+ PendingMessage message,
+ AbusiveOriginPermissionRevocationRequest::Outcome outcome) {
+ abusive_origin_revocation_request_.reset();
+
+ base::UmaHistogramLongTimes("PushMessaging.CheckOriginForAbuseTime",
+ base::Time::Now() - message.received_time);
+
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile_, message.app_id);
+
+ if (app_identifier.is_null()) {
+ CheckOriginForAbuseAndDispatchNextMessage();
+ return;
+ }
+
+ const GURL& origin = app_identifier.origin();
+ int64_t service_worker_registration_id =
+ app_identifier.service_worker_registration_id();
+
+ // It is possible that Notifications permission has been revoked by an user
+ // during abusive origin verification.
+ if (outcome == AbusiveOriginPermissionRevocationRequest::Outcome::
+ PERMISSION_NOT_REVOKED &&
+ IsPermissionSet(origin)) {
+ std::queue<PendingMessage>& delivery_queue =
+ message_delivery_queue_[{origin, service_worker_registration_id}];
+ delivery_queue.push(std::move(message));
+
+ // Start delivering push messages to this service worker if this was the
+ // first message. Otherwise just enqueue the message to be delivered once
+ // all previous messages have been handled.
+ if (delivery_queue.size() == 1) {
+ DeliverNextQueuedMessageForServiceWorkerRegistration(
+ origin, service_worker_registration_id);
+ }
+ } else {
+ // Drop message and unregister if origin has lost push permission.
+ DeliverMessageCallback(
+ message.app_id, origin, service_worker_registration_id, message.message,
+ false /* did_enqueue_message */,
+ outcome == AbusiveOriginPermissionRevocationRequest::Outcome::
+ PERMISSION_NOT_REVOKED
+ ? blink::mojom::PushEventStatus::PERMISSION_DENIED
+ : blink::mojom::PushEventStatus::PERMISSION_REVOKED_ABUSIVE);
+ }
+
+ // Verify the next message in the queue.
+ CheckOriginForAbuseAndDispatchNextMessage();
+}
+
+void PushMessagingServiceImpl::
+ DeliverNextQueuedMessageForServiceWorkerRegistration(
+ const GURL& origin,
+ int64_t service_worker_registration_id) {
+ MessageDeliveryQueueKey key{origin, service_worker_registration_id};
+ auto iter = message_delivery_queue_.find(key);
+ if (iter == message_delivery_queue_.end())
+ return;
+
+ const std::queue<PendingMessage>& delivery_queue = iter->second;
+ CHECK(!delivery_queue.empty());
+ const PendingMessage& next_message = delivery_queue.front();
+
+ const std::string& app_id = next_message.app_id;
+ const gcm::IncomingMessage& message = next_message.message;
+
+ auto deliver_message_callback = base::BindOnce(
+ &PushMessagingServiceImpl::DeliverMessageCallback,
+ weak_factory_.GetWeakPtr(), app_id, origin,
+ service_worker_registration_id, message, true /* did_enqueue_message */);
+
+ // It is possible that Notification permissions have been revoked by a user
+ // while handling previous messages for |origin|.
+ if (!IsPermissionSet(origin)) {
+ std::move(deliver_message_callback)
+ .Run(blink::mojom::PushEventStatus::PERMISSION_DENIED);
+ return;
+ }
+
+ // The payload of a push message can be valid with content, valid with empty
+ // content, or null.
+ absl::optional<std::string> payload;
+ if (message.decrypted)
+ payload = message.raw_data;
+
+ base::UmaHistogramLongTimes("PushMessaging.DeliverQueuedMessageTime",
+ base::Time::Now() - next_message.received_time);
+
+ // Inform tests observing message dispatching about the event.
+ if (message_dispatched_callback_for_testing_) {
+ message_dispatched_callback_for_testing_.Run(
+ app_id, origin, service_worker_registration_id, std::move(payload),
+ std::move(deliver_message_callback));
+ return;
+ }
+
+ // Dispatch the message to the appropriate Service Worker.
+ profile_->DeliverPushMessage(origin, service_worker_registration_id,
+ message.message_id, payload,
+ std::move(deliver_message_callback));
+}
+
+void PushMessagingServiceImpl::DeliverMessageCallback(
+ const std::string& app_id,
+ const GURL& requesting_origin,
+ int64_t service_worker_registration_id,
+ const gcm::IncomingMessage& message,
+ bool did_enqueue_message,
+ blink::mojom::PushEventStatus status) {
+ RecordDeliveryStatus(status);
+
+ // Note: It's important that |message_handled_callback| is run or passed to
+ // another function before this function returns.
+ auto message_handled_callback =
+ base::BindOnce(&PushMessagingServiceImpl::DidHandleMessage,
+ weak_factory_.GetWeakPtr(), app_id, message.message_id);
+
+ if (did_enqueue_message) {
+ message_handled_callback = base::BindOnce(
+ &PushMessagingServiceImpl::DidHandleEnqueuedMessage,
+ weak_factory_.GetWeakPtr(), requesting_origin,
+ service_worker_registration_id, std::move(message_handled_callback));
+ }
+
+ // A reason to automatically unsubscribe. UNKNOWN means do not unsubscribe.
+ blink::mojom::PushUnregistrationReason unsubscribe_reason =
+ blink::mojom::PushUnregistrationReason::UNKNOWN;
+
+ // TODO(mvanouwerkerk): Show a warning in the developer console of the
+ // Service Worker corresponding to app_id (and/or on an internals page).
+ // See https://crbug.com/508516 for options.
+ switch (status) {
+ // Call EnforceUserVisibleOnlyRequirements if the message was delivered to
+ // the Service Worker JavaScript, even if the website's event handler failed
+ // (to prevent sites deliberately failing in order to avoid having to show
+ // notifications).
+ case blink::mojom::PushEventStatus::SUCCESS:
+ case blink::mojom::PushEventStatus::EVENT_WAITUNTIL_REJECTED:
+ case blink::mojom::PushEventStatus::TIMEOUT:
+ // Only enforce the user visible requirements if silent push has not been
+ // enabled through a command line flag.
+ if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
+ switches::kAllowSilentPush)) {
+ notification_manager_.EnforceUserVisibleOnlyRequirements(
+ requesting_origin, service_worker_registration_id,
+ std::move(message_handled_callback));
+ message_handled_callback = base::OnceCallback<void(bool)>();
+ }
+ break;
+ case blink::mojom::PushEventStatus::SERVICE_WORKER_ERROR:
+ // Do nothing, and hope the error is transient.
+ break;
+ case blink::mojom::PushEventStatus::UNKNOWN_APP_ID:
+ unsubscribe_reason =
+ blink::mojom::PushUnregistrationReason::DELIVERY_UNKNOWN_APP_ID;
+ break;
+ case blink::mojom::PushEventStatus::PERMISSION_DENIED:
+ unsubscribe_reason =
+ blink::mojom::PushUnregistrationReason::DELIVERY_PERMISSION_DENIED;
+ break;
+ case blink::mojom::PushEventStatus::NO_SERVICE_WORKER:
+ unsubscribe_reason =
+ blink::mojom::PushUnregistrationReason::DELIVERY_NO_SERVICE_WORKER;
+ break;
+ case blink::mojom::PushEventStatus::PERMISSION_REVOKED_ABUSIVE:
+ unsubscribe_reason =
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED_ABUSIVE;
+ break;
+ }
+
+ // If |message_handled_callback| was not yet used, make a
+ // |completion_closure_runner| which should run by default at the end of this
+ // function, unless it is explicitly passed to another function or disabled.
+ base::ScopedClosureRunner completion_closure_runner(
+ message_handled_callback
+ ? base::BindOnce(std::move(message_handled_callback),
+ false /* did_show_generic_notification */)
+ : base::DoNothing());
+
+ if (unsubscribe_reason != blink::mojom::PushUnregistrationReason::UNKNOWN) {
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile_, app_id);
+ UnsubscribeInternal(
+ unsubscribe_reason,
+ app_identifier.is_null() ? GURL::EmptyGURL() : app_identifier.origin(),
+ app_identifier.is_null()
+ ? -1 /* kInvalidServiceWorkerRegistrationId */
+ : app_identifier.service_worker_registration_id(),
+ app_id, message.sender_id,
+ base::BindOnce(&UnregisterCallbackToClosure,
+ completion_closure_runner.Release()));
+
+ if (app_identifier.is_null())
+ return;
+
+ if (auto* devtools_context = GetDevToolsContext(app_identifier.origin())) {
+ std::stringstream ss;
+ ss << unsubscribe_reason;
+ devtools_context->LogBackgroundServiceEvent(
+ app_identifier.service_worker_registration_id(),
+ url::Origin::Create(app_identifier.origin()),
+ content::DevToolsBackgroundService::kPushMessaging,
+ "Unsubscribed due to error" /* event_name */, message.message_id,
+ {{"Reason", ss.str()}});
+ }
+ }
+}
+
+void PushMessagingServiceImpl::DidHandleEnqueuedMessage(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ base::OnceCallback<void(bool)> message_handled_callback,
+ bool did_show_generic_notification) {
+ // Lookup the message queue for the correct service worker.
+ MessageDeliveryQueueKey key{origin, service_worker_registration_id};
+ auto iter = message_delivery_queue_.find(key);
+ CHECK(iter != message_delivery_queue_.end());
+
+ // Remove the delivered message from the queue.
+ std::queue<PendingMessage>& delivery_queue = iter->second;
+ CHECK(!delivery_queue.empty());
+
+ base::UmaHistogramLongTimes(
+ "PushMessaging.MessageHandledTime",
+ base::Time::Now() - delivery_queue.front().received_time);
+
+ delivery_queue.pop();
+ if (delivery_queue.empty())
+ message_delivery_queue_.erase(key);
+
+ // This will call PushMessagingServiceImpl::DidHandleMessage().
+ std::move(message_handled_callback).Run(did_show_generic_notification);
+
+ // Deliver next message to this service worker now. We deliver them in series
+ // so we can check the visibility requirements after each message.
+ DeliverNextQueuedMessageForServiceWorkerRegistration(
+ origin, service_worker_registration_id);
+}
+
+void PushMessagingServiceImpl::DidHandleMessage(
+ const std::string& app_id,
+ const std::string& push_message_id,
+ bool did_show_generic_notification) {
+#if BUILDFLAG(ENABLE_BACKGROUND_MODE)
+ // Reset before running callbacks below, so tests can verify keep-alive reset.
+ if (message_delivery_queue_.empty()) {
+ in_flight_keep_alive_.reset();
+ in_flight_profile_keep_alive_.reset();
+ }
+#endif
+
+ if (message_callback_for_testing_)
+ message_callback_for_testing_.Run();
+
+#if defined(OS_ANDROID)
+ chrome::android::Java_PushMessagingServiceObserver_onMessageHandled(
+ base::android::AttachCurrentThread());
+#endif
+
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile_, app_id);
+
+ if (app_identifier.is_null() || !did_show_generic_notification)
+ return;
+
+ if (auto* devtools_context = GetDevToolsContext(app_identifier.origin())) {
+ devtools_context->LogBackgroundServiceEvent(
+ app_identifier.service_worker_registration_id(),
+ url::Origin::Create(app_identifier.origin()),
+ content::DevToolsBackgroundService::kPushMessaging,
+ "Generic notification shown" /* event_name */, push_message_id,
+ {} /* event_metadata */);
+ }
+}
+
+void PushMessagingServiceImpl::SetMessageCallbackForTesting(
+ const base::RepeatingClosure& callback) {
+ message_callback_for_testing_ = callback;
+}
+
+// Other gcm::GCMAppHandler methods --------------------------------------------
+
+void PushMessagingServiceImpl::OnMessagesDeleted(const std::string& app_id) {
+ // TODO(mvanouwerkerk): Consider firing an event on the Service Worker
+ // corresponding to |app_id| to inform the app about deleted messages.
+}
+
+void PushMessagingServiceImpl::OnSendError(
+ const std::string& app_id,
+ const gcm::GCMClient::SendErrorDetails& send_error_details) {
+ NOTREACHED() << "The Push API shouldn't have sent messages upstream";
+}
+
+void PushMessagingServiceImpl::OnSendAcknowledged(
+ const std::string& app_id,
+ const std::string& message_id) {
+ NOTREACHED() << "The Push API shouldn't have sent messages upstream";
+}
+
+void PushMessagingServiceImpl::OnMessageDecryptionFailed(
+ const std::string& app_id,
+ const std::string& message_id,
+ const std::string& error_message) {
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile_, app_id);
+
+ if (app_identifier.is_null())
+ return;
+
+ LogMessageReceivedEventToDevTools(
+ GetDevToolsContext(app_identifier.origin()), app_identifier, message_id,
+ /* was_encrypted= */ true, error_message, "" /* payload */);
+}
+
+// Subscribe and GetPermissionStatus methods -----------------------------------
+
+void PushMessagingServiceImpl::SubscribeFromDocument(
+ const GURL& requesting_origin,
+ int64_t service_worker_registration_id,
+ int render_process_id,
+ int render_frame_id,
+ blink::mojom::PushSubscriptionOptionsPtr options,
+ bool user_gesture,
+ RegisterCallback callback) {
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile_, requesting_origin, service_worker_registration_id);
+
+ // If there is no existing app identifier for the given Service Worker,
+ // generate a new one. This will create a new subscription on the server.
+ if (app_identifier.is_null()) {
+ app_identifier = PushMessagingAppIdentifier::Generate(
+ requesting_origin, service_worker_registration_id);
+ }
+
+ if (push_subscription_count_ + pending_push_subscription_count_ >=
+ kMaxRegistrations) {
+ SubscribeEndWithError(std::move(callback),
+ blink::mojom::PushRegistrationStatus::LIMIT_REACHED);
+ return;
+ }
+
+ content::RenderFrameHost* render_frame_host =
+ content::RenderFrameHost::FromID(render_process_id, render_frame_id);
+
+ if (!render_frame_host) {
+ // It is possible for `render_frame_host` to be nullptr here due to a race
+ // (crbug.com/1057981).
+ SubscribeEndWithError(
+ std::move(callback),
+ blink::mojom::PushRegistrationStatus::RENDERER_SHUTDOWN);
+ return;
+ }
+
+ if (!options->user_visible_only) {
+ render_frame_host->AddMessageToConsole(
+ blink::mojom::ConsoleMessageLevel::kError,
+ kSilentPushUnsupportedMessage);
+
+ SubscribeEndWithError(
+ std::move(callback),
+ blink::mojom::PushRegistrationStatus::PERMISSION_DENIED);
+ return;
+ }
+
+ // Push does not allow permission requests from iframes.
+ PermissionManagerFactory::GetForProfile(profile_)->RequestPermission(
+ ContentSettingsType::NOTIFICATIONS, render_frame_host, requesting_origin,
+ user_gesture,
+ base::BindOnce(&PushMessagingServiceImpl::DoSubscribe,
+ weak_factory_.GetWeakPtr(), std::move(app_identifier),
+ std::move(options), std::move(callback), render_process_id,
+ render_frame_id));
+}
+
+void PushMessagingServiceImpl::SubscribeFromWorker(
+ const GURL& requesting_origin,
+ int64_t service_worker_registration_id,
+ blink::mojom::PushSubscriptionOptionsPtr options,
+ RegisterCallback register_callback) {
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile_, requesting_origin, service_worker_registration_id);
+
+ // If there is no existing app identifier for the given Service Worker,
+ // generate a new one. This will create a new subscription on the server.
+ if (app_identifier.is_null()) {
+ app_identifier = PushMessagingAppIdentifier::Generate(
+ requesting_origin, service_worker_registration_id);
+ }
+
+ if (push_subscription_count_ + pending_push_subscription_count_ >=
+ kMaxRegistrations) {
+ SubscribeEndWithError(std::move(register_callback),
+ blink::mojom::PushRegistrationStatus::LIMIT_REACHED);
+ return;
+ }
+
+ if (!IsPermissionSet(requesting_origin, options->user_visible_only)) {
+ SubscribeEndWithError(
+ std::move(register_callback),
+ blink::mojom::PushRegistrationStatus::PERMISSION_DENIED);
+ return;
+ }
+
+ DoSubscribe(std::move(app_identifier), std::move(options),
+ std::move(register_callback),
+ /* render_process_id= */ -1, /* render_frame_id= */ -1,
+ CONTENT_SETTING_ALLOW);
+}
+
+blink::mojom::PermissionStatus PushMessagingServiceImpl::GetPermissionStatus(
+ const GURL& origin,
+ bool user_visible) {
+ if (!user_visible)
+ return blink::mojom::PermissionStatus::DENIED;
+
+ // Because the Push API is tied to Service Workers, many usages of the API
+ // won't have an embedding origin at all. Only consider the requesting
+ // |origin| when checking whether permission to use the API has been granted.
+ return ToPermissionStatus(
+ PermissionManagerFactory::GetForProfile(profile_)
+ ->GetPermissionStatus(ContentSettingsType::NOTIFICATIONS, origin,
+ origin)
+ .content_setting);
+}
+
+bool PushMessagingServiceImpl::SupportNonVisibleMessages() {
+ return false;
+}
+
+void PushMessagingServiceImpl::DoSubscribe(
+ PushMessagingAppIdentifier app_identifier,
+ blink::mojom::PushSubscriptionOptionsPtr options,
+ RegisterCallback register_callback,
+ int render_process_id,
+ int render_frame_id,
+ ContentSetting content_setting) {
+ if (content_setting != CONTENT_SETTING_ALLOW) {
+ SubscribeEndWithError(
+ std::move(register_callback),
+ blink::mojom::PushRegistrationStatus::PERMISSION_DENIED);
+ return;
+ }
+
+ std::string application_server_key_string(
+ options->application_server_key.begin(),
+ options->application_server_key.end());
+
+ // TODO(peter): Move this check to the renderer process & Mojo message
+ // validation once the flag is always enabled, and remove the
+ // |render_process_id| and |render_frame_id| parameters from this method.
+ if (!push_messaging::IsVapidKey(application_server_key_string)) {
+ content::RenderFrameHost* render_frame_host =
+ content::RenderFrameHost::FromID(render_process_id, render_frame_id);
+ if (base::FeatureList::IsEnabled(
+ features::kPushMessagingDisallowSenderIDs)) {
+ if (render_frame_host) {
+ render_frame_host->AddMessageToConsole(
+ blink::mojom::ConsoleMessageLevel::kError,
+ kSenderIdRegistrationDisallowedMessage);
+ }
+ SubscribeEndWithError(
+ std::move(register_callback),
+ blink::mojom::PushRegistrationStatus::UNSUPPORTED_GCM_SENDER_ID);
+ return;
+ } else if (render_frame_host) {
+ render_frame_host->AddMessageToConsole(
+ blink::mojom::ConsoleMessageLevel::kWarning,
+ kSenderIdRegistrationDeprecatedMessage);
+ }
+ }
+
+ IncreasePushSubscriptionCount(1, true /* is_pending */);
+
+ // Set time to live for GCM registration
+ base::TimeDelta ttl = base::TimeDelta();
+
+ if (base::FeatureList::IsEnabled(
+ features::kPushSubscriptionWithExpirationTime)) {
+ app_identifier.set_expiration_time(
+ base::Time::Now() + kPushSubscriptionExpirationPeriodTimeDelta);
+ DCHECK(app_identifier.expiration_time());
+ ttl = kPushSubscriptionExpirationPeriodTimeDelta;
+ }
+
+ GetInstanceIDDriver()
+ ->GetInstanceID(app_identifier.app_id())
+ ->GetToken(
+ push_messaging::NormalizeSenderInfo(application_server_key_string),
+ kGCMScope, ttl, {} /* flags */,
+ base::BindOnce(&PushMessagingServiceImpl::DidSubscribe,
+ weak_factory_.GetWeakPtr(), app_identifier,
+ application_server_key_string,
+ std::move(register_callback)));
+}
+
+void PushMessagingServiceImpl::SubscribeEnd(
+ RegisterCallback callback,
+ const std::string& subscription_id,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh,
+ const std::vector<uint8_t>& auth,
+ blink::mojom::PushRegistrationStatus status) {
+ std::move(callback).Run(subscription_id, endpoint, expiration_time, p256dh,
+ auth, status);
+}
+
+void PushMessagingServiceImpl::SubscribeEndWithError(
+ RegisterCallback callback,
+ blink::mojom::PushRegistrationStatus status) {
+ SubscribeEnd(std::move(callback), std::string() /* subscription_id */,
+ GURL::EmptyGURL() /* endpoint */,
+ absl::nullopt /* expiration_time */,
+ std::vector<uint8_t>() /* p256dh */,
+ std::vector<uint8_t>() /* auth */, status);
+}
+
+void PushMessagingServiceImpl::DidSubscribe(
+ const PushMessagingAppIdentifier& app_identifier,
+ const std::string& sender_id,
+ RegisterCallback callback,
+ const std::string& subscription_id,
+ InstanceID::Result result) {
+ DecreasePushSubscriptionCount(1, true /* was_pending */);
+
+ blink::mojom::PushRegistrationStatus status =
+ blink::mojom::PushRegistrationStatus::SERVICE_ERROR;
+
+ switch (result) {
+ case InstanceID::SUCCESS: {
+ const GURL endpoint = push_messaging::CreateEndpoint(subscription_id);
+
+ // Make sure that this subscription has associated encryption keys prior
+ // to returning it to the developer - they'll need this information in
+ // order to send payloads to the user.
+ GetEncryptionInfoForAppId(
+ app_identifier.app_id(), sender_id,
+ base::BindOnce(
+ &PushMessagingServiceImpl::DidSubscribeWithEncryptionInfo,
+ weak_factory_.GetWeakPtr(), app_identifier, std::move(callback),
+ subscription_id, endpoint));
+ return;
+ }
+ case InstanceID::INVALID_PARAMETER:
+ case InstanceID::DISABLED:
+ case InstanceID::ASYNC_OPERATION_PENDING:
+ case InstanceID::SERVER_ERROR:
+ case InstanceID::UNKNOWN_ERROR:
+ DLOG(ERROR) << "Push messaging subscription failed; InstanceID::Result = "
+ << result;
+ status = blink::mojom::PushRegistrationStatus::SERVICE_ERROR;
+ break;
+ case InstanceID::NETWORK_ERROR:
+ status = blink::mojom::PushRegistrationStatus::NETWORK_ERROR;
+ break;
+ }
+
+ SubscribeEndWithError(std::move(callback), status);
+}
+
+void PushMessagingServiceImpl::DidSubscribeWithEncryptionInfo(
+ const PushMessagingAppIdentifier& app_identifier,
+ RegisterCallback callback,
+ const std::string& subscription_id,
+ const GURL& endpoint,
+ std::string p256dh,
+ std::string auth_secret) {
+ if (p256dh.empty()) {
+ SubscribeEndWithError(
+ std::move(callback),
+ blink::mojom::PushRegistrationStatus::PUBLIC_KEY_UNAVAILABLE);
+ return;
+ }
+
+ app_identifier.PersistToPrefs(profile_);
+
+ IncreasePushSubscriptionCount(1, false /* is_pending */);
+
+ SubscribeEnd(std::move(callback), subscription_id, endpoint,
+ app_identifier.expiration_time(),
+ std::vector<uint8_t>(p256dh.begin(), p256dh.end()),
+ std::vector<uint8_t>(auth_secret.begin(), auth_secret.end()),
+ blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE);
+}
+
+// GetSubscriptionInfo methods -------------------------------------------------
+
+void PushMessagingServiceImpl::GetSubscriptionInfo(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const std::string& sender_id,
+ const std::string& subscription_id,
+ SubscriptionInfoCallback callback) {
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile_, origin, service_worker_registration_id);
+
+ if (app_identifier.is_null()) {
+ std::move(callback).Run(
+ false /* is_valid */, GURL::EmptyGURL() /*endpoint*/,
+ absl::nullopt /* expiration_time */,
+ std::vector<uint8_t>() /* p256dh */, std::vector<uint8_t>() /* auth */);
+ return;
+ }
+
+ const GURL endpoint = push_messaging::CreateEndpoint(subscription_id);
+ const std::string& app_id = app_identifier.app_id();
+ absl::optional<base::Time> expiration_time = app_identifier.expiration_time();
+
+ base::OnceCallback<void(bool)> validate_cb =
+ base::BindOnce(&PushMessagingServiceImpl::DidValidateSubscription,
+ weak_factory_.GetWeakPtr(), app_id, sender_id, endpoint,
+ expiration_time, std::move(callback));
+
+ if (PushMessagingAppIdentifier::UseInstanceID(app_id)) {
+ GetInstanceIDDriver()->GetInstanceID(app_id)->ValidateToken(
+ push_messaging::NormalizeSenderInfo(sender_id), kGCMScope,
+ subscription_id, std::move(validate_cb));
+ } else {
+ GetGCMDriver()->ValidateRegistration(
+ app_id, {push_messaging::NormalizeSenderInfo(sender_id)},
+ subscription_id, std::move(validate_cb));
+ }
+}
+
+void PushMessagingServiceImpl::DidValidateSubscription(
+ const std::string& app_id,
+ const std::string& sender_id,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ SubscriptionInfoCallback callback,
+ bool is_valid) {
+ if (!is_valid) {
+ std::move(callback).Run(
+ false /* is_valid */, GURL::EmptyGURL() /* endpoint */,
+ absl::nullopt /* expiration_time */,
+ std::vector<uint8_t>() /* p256dh */, std::vector<uint8_t>() /* auth */);
+ return;
+ }
+
+ GetEncryptionInfoForAppId(
+ app_id, sender_id,
+ base::BindOnce(&PushMessagingServiceImpl::DidGetEncryptionInfo,
+ weak_factory_.GetWeakPtr(), endpoint, expiration_time,
+ std::move(callback)));
+}
+
+void PushMessagingServiceImpl::DidGetEncryptionInfo(
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ SubscriptionInfoCallback callback,
+ std::string p256dh,
+ std::string auth_secret) const {
+ // I/O errors might prevent the GCM Driver from retrieving a key-pair.
+ bool is_valid = !p256dh.empty();
+ std::move(callback).Run(
+ is_valid, endpoint, expiration_time,
+ std::vector<uint8_t>(p256dh.begin(), p256dh.end()),
+ std::vector<uint8_t>(auth_secret.begin(), auth_secret.end()));
+}
+
+// Unsubscribe methods ---------------------------------------------------------
+
+void PushMessagingServiceImpl::Unsubscribe(
+ blink::mojom::PushUnregistrationReason reason,
+ const GURL& requesting_origin,
+ int64_t service_worker_registration_id,
+ const std::string& sender_id,
+ UnregisterCallback callback) {
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile_, requesting_origin, service_worker_registration_id);
+
+ UnsubscribeInternal(
+ reason, requesting_origin, service_worker_registration_id,
+ app_identifier.is_null() ? std::string() : app_identifier.app_id(),
+ sender_id, std::move(callback));
+}
+
+void PushMessagingServiceImpl::UnsubscribeInternal(
+ blink::mojom::PushUnregistrationReason reason,
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const std::string& app_id,
+ const std::string& sender_id,
+ UnregisterCallback callback) {
+ DCHECK(!app_id.empty() || (!origin.is_empty() &&
+ service_worker_registration_id !=
+ -1 /* kInvalidServiceWorkerRegistrationId */))
+ << "Need an app_id and/or origin+service_worker_registration_id";
+
+ RecordUnsubscribeReason(reason);
+
+ if (origin.is_empty() ||
+ service_worker_registration_id ==
+ -1 /* kInvalidServiceWorkerRegistrationId */) {
+ // Can't clear Service Worker database.
+ DidClearPushSubscriptionId(reason, app_id, sender_id, std::move(callback));
+ return;
+ }
+ ClearPushSubscriptionId(
+ profile_, origin, service_worker_registration_id,
+ base::BindOnce(&PushMessagingServiceImpl::DidClearPushSubscriptionId,
+ weak_factory_.GetWeakPtr(), reason, app_id, sender_id,
+ std::move(callback)));
+}
+
+void PushMessagingServiceImpl::DidClearPushSubscriptionId(
+ blink::mojom::PushUnregistrationReason reason,
+ const std::string& app_id,
+ const std::string& sender_id,
+ UnregisterCallback callback) {
+ if (app_id.empty()) {
+ // Without an |app_id|, we can neither delete the subscription from the
+ // PushMessagingAppIdentifier map, nor unsubscribe with the GCM Driver.
+ std::move(callback).Run(
+ blink::mojom::PushUnregistrationStatus::SUCCESS_WAS_NOT_REGISTERED);
+ return;
+ }
+
+ // Delete the mapping for this app_id, to guarantee that no messages get
+ // delivered in future (even if unregistration fails).
+ // TODO(johnme): Instead of deleting these app ids, store them elsewhere, and
+ // retry unregistration if it fails due to network errors (crbug.com/465399).
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile_, app_id);
+ bool was_subscribed = !app_identifier.is_null();
+ if (was_subscribed)
+ app_identifier.DeleteFromPrefs(profile_);
+
+ // Run the unsubscribe callback *before* asking the InstanceIDDriver/GCMDriver
+ // to unsubscribe, since that's a slow process involving network retries, and
+ // by this point enough local state has been deleted that the subscription is
+ // inactive. Note that DeliverMessageCallback automatically unsubscribes if
+ // messages are later received for a subscription that was locally deleted,
+ // so as long as messages keep getting sent to it, the unsubscription should
+ // eventually reach GCM servers even if this particular attempt fails.
+ std::move(callback).Run(
+ was_subscribed
+ ? blink::mojom::PushUnregistrationStatus::SUCCESS_UNREGISTERED
+ : blink::mojom::PushUnregistrationStatus::SUCCESS_WAS_NOT_REGISTERED);
+
+ if (PushMessagingAppIdentifier::UseInstanceID(app_id)) {
+ GetInstanceIDDriver()->GetInstanceID(app_id)->DeleteID(
+ base::BindOnce(&PushMessagingServiceImpl::DidDeleteID,
+ weak_factory_.GetWeakPtr(), app_id, was_subscribed));
+
+ } else {
+ auto unregister_callback =
+ base::BindOnce(&PushMessagingServiceImpl::DidUnregister,
+ weak_factory_.GetWeakPtr(), was_subscribed);
+#if defined(OS_ANDROID)
+ // On Android the backend is different, and requires the original sender_id.
+ // DidGetSenderIdUnexpectedUnsubscribe and
+ // DidDeleteServiceWorkerRegistration sometimes call us with an empty one.
+ if (sender_id.empty()) {
+ std::move(unregister_callback).Run(gcm::GCMClient::INVALID_PARAMETER);
+ } else {
+ GetGCMDriver()->UnregisterWithSenderId(
+ app_id, push_messaging::NormalizeSenderInfo(sender_id),
+ std::move(unregister_callback));
+ }
+#else
+ GetGCMDriver()->Unregister(app_id, std::move(unregister_callback));
+#endif
+ }
+}
+
+void PushMessagingServiceImpl::DidUnregister(bool was_subscribed,
+ gcm::GCMClient::Result result) {
+ RecordUnsubscribeGCMResult(result);
+ DidUnsubscribe(std::string() /* app_id_when_instance_id */, was_subscribed);
+}
+
+void PushMessagingServiceImpl::DidDeleteID(const std::string& app_id,
+ bool was_subscribed,
+ InstanceID::Result result) {
+ RecordUnsubscribeIIDResult(result);
+ // DidUnsubscribe must be run asynchronously when passing a non-empty
+ // |app_id_when_instance_id|, since it calls
+ // InstanceIDDriver::RemoveInstanceID which deletes the InstanceID itself.
+ // Calling that immediately would cause a use-after-free in our caller.
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&PushMessagingServiceImpl::DidUnsubscribe,
+ weak_factory_.GetWeakPtr(), app_id, was_subscribed));
+}
+
+void PushMessagingServiceImpl::DidUnsubscribe(
+ const std::string& app_id_when_instance_id,
+ bool was_subscribed) {
+ if (!app_id_when_instance_id.empty())
+ GetInstanceIDDriver()->RemoveInstanceID(app_id_when_instance_id);
+
+ if (was_subscribed)
+ DecreasePushSubscriptionCount(1, false /* was_pending */);
+
+ if (!unsubscribe_callback_for_testing_.is_null())
+ std::move(unsubscribe_callback_for_testing_).Run();
+}
+
+void PushMessagingServiceImpl::SetUnsubscribeCallbackForTesting(
+ base::OnceClosure callback) {
+ unsubscribe_callback_for_testing_ = std::move(callback);
+}
+
+// DidDeleteServiceWorkerRegistration methods ----------------------------------
+
+void PushMessagingServiceImpl::DidDeleteServiceWorkerRegistration(
+ const GURL& origin,
+ int64_t service_worker_registration_id) {
+ const PushMessagingAppIdentifier& app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(
+ profile_, origin, service_worker_registration_id);
+ if (app_identifier.is_null()) {
+ if (!service_worker_unregistered_callback_for_testing_.is_null())
+ service_worker_unregistered_callback_for_testing_.Run();
+ return;
+ }
+ // Note this will not fully unsubscribe pre-InstanceID subscriptions on
+ // Android from GCM, as that requires a sender_id. (Ideally we'd fetch it
+ // from the SWDB in some "before_unregistered" SWObserver event.)
+ UnsubscribeInternal(
+ blink::mojom::PushUnregistrationReason::SERVICE_WORKER_UNREGISTERED,
+ origin, service_worker_registration_id, app_identifier.app_id(),
+ std::string() /* sender_id */,
+ base::BindOnce(&UnregisterCallbackToClosure,
+ service_worker_unregistered_callback_for_testing_.is_null()
+ ? base::DoNothing()
+ : service_worker_unregistered_callback_for_testing_));
+}
+
+void PushMessagingServiceImpl::SetServiceWorkerUnregisteredCallbackForTesting(
+ base::RepeatingClosure callback) {
+ service_worker_unregistered_callback_for_testing_ = std::move(callback);
+}
+
+// DidDeleteServiceWorkerDatabase methods --------------------------------------
+
+void PushMessagingServiceImpl::DidDeleteServiceWorkerDatabase() {
+ std::vector<PushMessagingAppIdentifier> app_identifiers =
+ PushMessagingAppIdentifier::GetAll(profile_);
+
+ base::RepeatingClosure completed_closure = base::BarrierClosure(
+ app_identifiers.size(),
+ service_worker_database_wiped_callback_for_testing_.is_null()
+ ? base::DoNothing()
+ : service_worker_database_wiped_callback_for_testing_);
+
+ for (const PushMessagingAppIdentifier& app_identifier : app_identifiers) {
+ // Note this will not fully unsubscribe pre-InstanceID subscriptions on
+ // Android from GCM, as that requires a sender_id. We can't fetch those from
+ // the Service Worker database anymore as it's been deleted.
+ UnsubscribeInternal(
+ blink::mojom::PushUnregistrationReason::SERVICE_WORKER_DATABASE_WIPED,
+ app_identifier.origin(),
+ app_identifier.service_worker_registration_id(),
+ app_identifier.app_id(), std::string() /* sender_id */,
+ base::BindOnce(&UnregisterCallbackToClosure, completed_closure));
+ }
+}
+
+void PushMessagingServiceImpl::SetServiceWorkerDatabaseWipedCallbackForTesting(
+ base::RepeatingClosure callback) {
+ service_worker_database_wiped_callback_for_testing_ = std::move(callback);
+}
+
+// OnContentSettingChanged methods ---------------------------------------------
+
+void PushMessagingServiceImpl::OnContentSettingChanged(
+ const ContentSettingsPattern& primary_pattern,
+ const ContentSettingsPattern& secondary_pattern,
+ ContentSettingsTypeSet content_type_set) {
+ DCHECK(primary_pattern.IsValid());
+ if (!content_type_set.Contains(ContentSettingsType::NOTIFICATIONS))
+ return;
+
+ std::vector<PushMessagingAppIdentifier> all_app_identifiers =
+ PushMessagingAppIdentifier::GetAll(profile_);
+
+ base::RepeatingClosure barrier_closure = base::BarrierClosure(
+ all_app_identifiers.size(),
+ content_setting_changed_callback_for_testing_.is_null()
+ ? base::DoNothing()
+ : content_setting_changed_callback_for_testing_);
+
+ for (const PushMessagingAppIdentifier& app_identifier : all_app_identifiers) {
+ if (!primary_pattern.Matches(app_identifier.origin())) {
+ barrier_closure.Run();
+ continue;
+ }
+
+ if (IsPermissionSet(app_identifier.origin())) {
+ barrier_closure.Run();
+ continue;
+ }
+
+ UnexpectedChange(app_identifier,
+ blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED,
+ barrier_closure);
+ }
+}
+
+void PushMessagingServiceImpl::UnexpectedUnsubscribe(
+ const PushMessagingAppIdentifier& app_identifier,
+ blink::mojom::PushUnregistrationReason reason,
+ UnregisterCallback unregister_callback) {
+ // When `pushsubscriptionchange` is supported by default, get |sender_id| from
+ // GetPushSubscriptionFromAppIdentifier callback and do not get the info from
+ // IO twice
+ bool need_sender_id = false;
+#if defined(OS_ANDROID)
+ need_sender_id =
+ !PushMessagingAppIdentifier::UseInstanceID(app_identifier.app_id());
+#endif
+ if (need_sender_id) {
+ GetSenderId(
+ profile_, app_identifier.origin(),
+ app_identifier.service_worker_registration_id(),
+ base::BindOnce(
+ &PushMessagingServiceImpl::DidGetSenderIdUnexpectedUnsubscribe,
+ weak_factory_.GetWeakPtr(), app_identifier, reason,
+ std::move(unregister_callback)));
+ } else {
+ UnsubscribeInternal(reason, app_identifier.origin(),
+ app_identifier.service_worker_registration_id(),
+ app_identifier.app_id(),
+ std::string() /* sender_id */,
+ std::move(unregister_callback));
+ }
+}
+
+void PushMessagingServiceImpl::GetPushSubscriptionFromAppIdentifier(
+ const PushMessagingAppIdentifier& app_identifier,
+ base::OnceCallback<void(blink::mojom::PushSubscriptionPtr)>
+ subscription_cb) {
+ GetSWData(profile_, app_identifier.origin(),
+ app_identifier.service_worker_registration_id(),
+ base::BindOnce(&PushMessagingServiceImpl::DidGetSWData,
+ weak_factory_.GetWeakPtr(), app_identifier,
+ std::move(subscription_cb)));
+}
+
+void PushMessagingServiceImpl::DidGetSWData(
+ const PushMessagingAppIdentifier& app_identifier,
+ base::OnceCallback<void(blink::mojom::PushSubscriptionPtr)> subscription_cb,
+ const std::string& sender_id,
+ const std::string& subscription_id) {
+ // SW Database was corrupted, return immediately
+ if (sender_id.empty() || subscription_id.empty()) {
+ std::move(subscription_cb).Run(nullptr /* subscription */);
+ return;
+ }
+ GetSubscriptionInfo(
+ app_identifier.origin(), app_identifier.service_worker_registration_id(),
+ sender_id, subscription_id,
+ base::BindOnce(
+ &PushMessagingServiceImpl::GetPushSubscriptionFromAppIdentifierEnd,
+ weak_factory_.GetWeakPtr(), std::move(subscription_cb), sender_id));
+}
+
+void PushMessagingServiceImpl::GetPushSubscriptionFromAppIdentifierEnd(
+ base::OnceCallback<void(blink::mojom::PushSubscriptionPtr)> callback,
+ const std::string& sender_id,
+ bool is_valid,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh,
+ const std::vector<uint8_t>& auth) {
+ if (!is_valid) {
+ // TODO(viviy): Log error in UMA
+ std::move(callback).Run(nullptr /* subscription */);
+ return;
+ }
+
+ std::move(callback).Run(blink::mojom::PushSubscription::New(
+ endpoint, expiration_time, push_messaging::MakeOptions(sender_id), p256dh,
+ auth));
+}
+
+void PushMessagingServiceImpl::FirePushSubscriptionChange(
+ const PushMessagingAppIdentifier& app_identifier,
+ base::OnceClosure completed_closure,
+ blink::mojom::PushSubscriptionPtr new_subscription,
+ blink::mojom::PushSubscriptionPtr old_subscription) {
+ // Ensure |completed_closure| is run after this function
+ base::ScopedClosureRunner scoped_closure(std::move(completed_closure));
+
+ if (!base::FeatureList::IsEnabled(features::kPushSubscriptionChangeEvent))
+ return;
+
+ if (app_identifier.is_null()) {
+ FirePushSubscriptionChangeCallback(
+ app_identifier, blink::mojom::PushEventStatus::UNKNOWN_APP_ID);
+ return;
+ }
+
+ profile_->FirePushSubscriptionChangeEvent(
+ app_identifier.origin(), app_identifier.service_worker_registration_id(),
+ std::move(new_subscription), std::move(old_subscription),
+ base::BindOnce(
+ &PushMessagingServiceImpl::FirePushSubscriptionChangeCallback,
+ weak_factory_.GetWeakPtr(), app_identifier));
+}
+
+void PushMessagingServiceImpl::FirePushSubscriptionChangeCallback(
+ const PushMessagingAppIdentifier& app_identifier,
+ blink::mojom::PushEventStatus status) {
+ // Log Data in UMA
+ RecordPushSubcriptionChangeStatus(status);
+}
+
+void PushMessagingServiceImpl::DidGetSenderIdUnexpectedUnsubscribe(
+ const PushMessagingAppIdentifier& app_identifier,
+ blink::mojom::PushUnregistrationReason reason,
+ UnregisterCallback callback,
+ const std::string& sender_id) {
+ // Unsubscribe the PushMessagingAppIdentifier with the push service.
+ // It's possible for GetSenderId to have failed and sender_id to be empty, if
+ // cookies (and the SW database) for an origin got cleared before permissions
+ // are cleared for the origin. In that case for legacy GCM registrations on
+ // Android, Unsubscribe will just delete the app identifier to block future
+ // messages.
+ // TODO(johnme): Auto-unregister before SW DB is cleared (crbug.com/402458).
+ UnsubscribeInternal(reason, app_identifier.origin(),
+ app_identifier.service_worker_registration_id(),
+ app_identifier.app_id(), sender_id, std::move(callback));
+}
+
+void PushMessagingServiceImpl::SetContentSettingChangedCallbackForTesting(
+ base::RepeatingClosure callback) {
+ content_setting_changed_callback_for_testing_ = std::move(callback);
+}
+
+// KeyedService methods -------------------------------------------------------
+
+void PushMessagingServiceImpl::Shutdown() {
+ GetGCMDriver()->RemoveAppHandler(kPushMessagingAppIdentifierPrefix);
+ HostContentSettingsMapFactory::GetForProfile(profile_)->RemoveObserver(this);
+}
+
+// content::NotificationObserver methods ---------------------------------------
+
+void PushMessagingServiceImpl::Observe(
+ int type,
+ const content::NotificationSource& source,
+ const content::NotificationDetails& details) {
+ DCHECK_EQ(chrome::NOTIFICATION_APP_TERMINATING, type);
+ shutdown_started_ = true;
+#if BUILDFLAG(ENABLE_BACKGROUND_MODE)
+ in_flight_keep_alive_.reset();
+ in_flight_profile_keep_alive_.reset();
+#endif // BUILDFLAG(ENABLE_BACKGROUND_MODE)
+}
+
+// OnSubscriptionInvalidation methods ------------------------------------------
+
+void PushMessagingServiceImpl::OnSubscriptionInvalidation(
+ const std::string& app_id) {
+ DCHECK(base::FeatureList::IsEnabled(features::kPushSubscriptionChangeEvent))
+ << "It is not allowed to call this method when "
+ "features::kPushSubscriptionChangeEvent is disabled.";
+ PushMessagingAppIdentifier old_app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile_, app_id);
+ if (old_app_identifier.is_null())
+ return;
+
+ GetSenderId(profile_, old_app_identifier.origin(),
+ old_app_identifier.service_worker_registration_id(),
+ base::BindOnce(&PushMessagingServiceImpl::GetOldSubscription,
+ weak_factory_.GetWeakPtr(), old_app_identifier));
+}
+
+void PushMessagingServiceImpl::GetOldSubscription(
+ PushMessagingAppIdentifier old_app_identifier,
+ const std::string& sender_id) {
+ GetPushSubscriptionFromAppIdentifier(
+ old_app_identifier,
+ base::BindOnce(&PushMessagingServiceImpl::StartRefresh,
+ weak_factory_.GetWeakPtr(), old_app_identifier,
+ sender_id));
+}
+
+void PushMessagingServiceImpl::StartRefresh(
+ PushMessagingAppIdentifier old_app_identifier,
+ const std::string& sender_id,
+ blink::mojom::PushSubscriptionPtr old_subscription) {
+ // Generate a new app_identifier with the same information, but a different
+ // app_id. Expiration time will be overwritten by DoSubscribe, if the flag
+ // features::kPushSubscriptionWithExpiration time is enabled
+ PushMessagingAppIdentifier new_app_identifier =
+ PushMessagingAppIdentifier::Generate(
+ old_app_identifier.origin(),
+ old_app_identifier.service_worker_registration_id(),
+ absl::nullopt /* expiration_time */);
+
+ refresher_.Refresh(old_app_identifier, new_app_identifier.app_id(),
+ sender_id);
+
+ UpdateSubscription(
+ new_app_identifier, push_messaging::MakeOptions(sender_id),
+ base::BindOnce(&PushMessagingServiceImpl::DidUpdateSubscription,
+ weak_factory_.GetWeakPtr(), new_app_identifier.app_id(),
+ old_app_identifier.app_id(), std::move(old_subscription),
+ sender_id));
+}
+
+void PushMessagingServiceImpl::UpdateSubscription(
+ PushMessagingAppIdentifier app_identifier,
+ blink::mojom::PushSubscriptionOptionsPtr options,
+ RegisterCallback callback) {
+ // After getting a new GCM registration, update the |subscription_id| in SW
+ // database before running the callback
+ auto register_callback = base::BindOnce(
+ [](RegisterCallback cb, Profile* profile, PushMessagingAppIdentifier ai,
+ const std::string& registration_id, const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh, const std::vector<uint8_t>& auth,
+ blink::mojom::PushRegistrationStatus status) {
+ base::OnceClosure closure =
+ base::BindOnce(std::move(cb), registration_id, endpoint,
+ expiration_time, p256dh, auth, status);
+ base::ScopedClosureRunner closure_runner(std::move(closure));
+ if (status ==
+ blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE) {
+ UpdatePushSubscriptionId(profile, ai.origin(),
+ ai.service_worker_registration_id(),
+ registration_id, closure_runner.Release());
+ }
+ },
+ std::move(callback), profile_, app_identifier);
+ // Subscribe using the new subscription information, this will overwrite
+ // the expiration time of |app_identifier|
+ DoSubscribe(app_identifier, std::move(options), std::move(register_callback),
+ -1 /* render_process_id */, -1 /* render_frame_id */,
+ CONTENT_SETTING_ALLOW);
+}
+
+void PushMessagingServiceImpl::DidUpdateSubscription(
+ const std::string& new_app_id,
+ const std::string& old_app_id,
+ blink::mojom::PushSubscriptionPtr old_subscription,
+ const std::string& sender_id,
+ const std::string& registration_id,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh,
+ const std::vector<uint8_t>& auth,
+ blink::mojom::PushRegistrationStatus status) {
+ // TODO(crbug.com/1122545): Currently, if |status| is unsuccessful, the old
+ // subscription remains in SW database and preferences and the refresh is
+ // aborted. Instead, one should abort the refresh and retry to refresh
+ // periodically.
+ if (status !=
+ blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE) {
+ return;
+ }
+
+ // Old subscription is now replaced locally by the new subscription
+ refresher_.OnSubscriptionUpdated(new_app_id);
+
+ PushMessagingAppIdentifier new_app_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile_, new_app_id);
+
+ // Callback for testing
+ base::OnceClosure callback =
+ (invalidation_callback_for_testing_)
+ ? std::move(invalidation_callback_for_testing_)
+ : base::DoNothing();
+
+ FirePushSubscriptionChange(
+ new_app_identifier, std::move(callback),
+ blink::mojom::PushSubscription::New(
+ endpoint, expiration_time, push_messaging::MakeOptions(sender_id),
+ p256dh, auth),
+ std::move(old_subscription));
+}
+
+// PushMessagingRefresher::Observer methods ------------------------------------
+
+void PushMessagingServiceImpl::OnOldSubscriptionExpired(
+ const std::string& app_id,
+ const std::string& sender_id) {
+ // Unsubscribe without clearing SW database, since values of the new
+ // subscription are already saved there.
+ // After unsubscribing, the refresher will get notified.
+ UnsubscribeInternal(
+ blink::mojom::PushUnregistrationReason::REFRESH_FINISHED,
+ GURL::EmptyGURL() /* origin */, -1 /* service_worker_registration_id */,
+ app_id, sender_id,
+ base::BindOnce(&UnregisterCallbackToClosure,
+ base::BindOnce(&PushMessagingRefresher::OnUnsubscribed,
+ refresher_.GetWeakPtr(), app_id)));
+}
+
+void PushMessagingServiceImpl::OnRefreshFinished(
+ const PushMessagingAppIdentifier& app_identifier) {
+ // TODO(viviy): Log data in UMA
+}
+
+void PushMessagingServiceImpl::SetInvalidationCallbackForTesting(
+ base::OnceClosure callback) {
+ invalidation_callback_for_testing_ = std::move(callback);
+}
+
+// Helper methods --------------------------------------------------------------
+
+void PushMessagingServiceImpl::SetRemoveExpiredSubscriptionsCallbackForTesting(
+ base::OnceClosure closure) {
+ remove_expired_subscriptions_callback_for_testing_ = std::move(closure);
+}
+
+// Assumes user_visible always since this is just meant to check
+// if the permission was previously granted and not revoked.
+bool PushMessagingServiceImpl::IsPermissionSet(const GURL& origin,
+ bool user_visible) {
+ return GetPermissionStatus(origin, user_visible) ==
+ blink::mojom::PermissionStatus::GRANTED;
+}
+
+void PushMessagingServiceImpl::GetEncryptionInfoForAppId(
+ const std::string& app_id,
+ const std::string& sender_id,
+ gcm::GCMEncryptionProvider::EncryptionInfoCallback callback) {
+ if (PushMessagingAppIdentifier::UseInstanceID(app_id)) {
+ GetInstanceIDDriver()->GetInstanceID(app_id)->GetEncryptionInfo(
+ push_messaging::NormalizeSenderInfo(sender_id), std::move(callback));
+ } else {
+ GetGCMDriver()->GetEncryptionInfo(app_id, std::move(callback));
+ }
+}
+
+gcm::GCMDriver* PushMessagingServiceImpl::GetGCMDriver() const {
+ gcm::GCMProfileService* gcm_profile_service =
+ gcm::GCMProfileServiceFactory::GetForProfile(profile_);
+ CHECK(gcm_profile_service);
+ CHECK(gcm_profile_service->driver());
+ return gcm_profile_service->driver();
+}
+
+instance_id::InstanceIDDriver* PushMessagingServiceImpl::GetInstanceIDDriver()
+ const {
+ instance_id::InstanceIDProfileService* instance_id_profile_service =
+ instance_id::InstanceIDProfileServiceFactory::GetForProfile(profile_);
+ CHECK(instance_id_profile_service);
+ CHECK(instance_id_profile_service->driver());
+ return instance_id_profile_service->driver();
+}
+
+content::DevToolsBackgroundServicesContext*
+PushMessagingServiceImpl::GetDevToolsContext(const GURL& origin) const {
+ auto* storage_partition = profile_->GetStoragePartitionForUrl(origin);
+ if (!storage_partition)
+ return nullptr;
+
+ auto* devtools_context =
+ storage_partition->GetDevToolsBackgroundServicesContext();
+
+ if (!devtools_context->IsRecording(
+ content::DevToolsBackgroundService::kPushMessaging)) {
+ return nullptr;
+ }
+
+ return devtools_context;
+}
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_impl.h b/chromium/chrome/browser/push_messaging/push_messaging_service_impl.h
new file mode 100644
index 00000000000..a99e6e578f4
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_service_impl.h
@@ -0,0 +1,475 @@
+// Copyright 2014 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_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_IMPL_H_
+#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_IMPL_H_
+
+#include <stdint.h>
+#include <memory>
+#include <queue>
+#include <utility>
+#include <vector>
+
+#include "base/callback.h"
+#include "base/callback_helpers.h"
+#include "base/containers/flat_map.h"
+#include "base/gtest_prod_util.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/scoped_observation.h"
+#include "base/time/time.h"
+#include "chrome/browser/permissions/abusive_origin_permission_revocation_request.h"
+#include "chrome/browser/push_messaging/push_messaging_notification_manager.h"
+#include "chrome/browser/push_messaging/push_messaging_refresher.h"
+#include "chrome/common/buildflags.h"
+#include "components/content_settings/core/browser/content_settings_observer.h"
+#include "components/content_settings/core/common/content_settings.h"
+#include "components/content_settings/core/common/content_settings_types.h"
+#include "components/gcm_driver/common/gcm_message.h"
+#include "components/gcm_driver/crypto/gcm_encryption_provider.h"
+#include "components/gcm_driver/gcm_app_handler.h"
+#include "components/gcm_driver/gcm_client.h"
+#include "components/gcm_driver/instance_id/instance_id.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "content/public/browser/notification_observer.h"
+#include "content/public/browser/notification_registrar.h"
+#include "content/public/browser/push_messaging_service.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/blink/public/mojom/push_messaging/push_messaging.mojom-forward.h"
+
+class GURL;
+class Profile;
+class PushMessagingAppIdentifier;
+class PushMessagingServiceTest;
+class ScopedKeepAlive;
+class ScopedProfileKeepAlive;
+
+namespace blink {
+namespace mojom {
+enum class PushEventStatus;
+enum class PushRegistrationStatus;
+} // namespace mojom
+} // namespace blink
+
+namespace content {
+class DevToolsBackgroundServicesContext;
+} // namespace content
+
+namespace gcm {
+class GCMDriver;
+} // namespace gcm
+
+namespace instance_id {
+class InstanceIDDriver;
+} // namespace instance_id
+
+namespace {
+struct PendingMessage {
+ PendingMessage(std::string app_id, gcm::IncomingMessage message);
+ PendingMessage(const PendingMessage& other);
+ PendingMessage(PendingMessage&& other);
+ ~PendingMessage();
+
+ PendingMessage& operator=(PendingMessage&& other);
+
+ std::string app_id;
+ gcm::IncomingMessage message;
+ base::Time received_time;
+};
+} // namespace
+
+class PushMessagingServiceImpl : public content::PushMessagingService,
+ public gcm::GCMAppHandler,
+ public content_settings::Observer,
+ public KeyedService,
+ public content::NotificationObserver,
+ public PushMessagingRefresher::Observer {
+ public:
+ // If any Service Workers are using push, starts GCM and adds an app handler.
+ static void InitializeForProfile(Profile* profile);
+
+ explicit PushMessagingServiceImpl(Profile* profile);
+
+ PushMessagingServiceImpl(const PushMessagingServiceImpl&) = delete;
+ PushMessagingServiceImpl& operator=(const PushMessagingServiceImpl&) = delete;
+
+ ~PushMessagingServiceImpl() override;
+
+ // Check and remove subscriptions that are expired when |this| is initialized
+ void RemoveExpiredSubscriptions();
+
+ // Gets the permission status for the given |origin|.
+ blink::mojom::PermissionStatus GetPermissionStatus(const GURL& origin,
+ bool user_visible);
+
+ // gcm::GCMAppHandler implementation.
+ void ShutdownHandler() override;
+ void OnStoreReset() override;
+ void OnMessage(const std::string& app_id,
+ const gcm::IncomingMessage& message) override;
+ void OnMessagesDeleted(const std::string& app_id) override;
+ void OnSendError(
+ const std::string& app_id,
+ const gcm::GCMClient::SendErrorDetails& send_error_details) override;
+ void OnSendAcknowledged(const std::string& app_id,
+ const std::string& message_id) override;
+ void OnMessageDecryptionFailed(const std::string& app_id,
+ const std::string& message_id,
+ const std::string& error_message) override;
+ bool CanHandle(const std::string& app_id) const override;
+
+ // content::PushMessagingService implementation:
+ void SubscribeFromDocument(const GURL& requesting_origin,
+ int64_t service_worker_registration_id,
+ int render_process_id,
+ int render_frame_id,
+ blink::mojom::PushSubscriptionOptionsPtr options,
+ bool user_gesture,
+ RegisterCallback callback) override;
+ void SubscribeFromWorker(const GURL& requesting_origin,
+ int64_t service_worker_registration_id,
+ blink::mojom::PushSubscriptionOptionsPtr options,
+ RegisterCallback callback) override;
+ void GetSubscriptionInfo(const GURL& origin,
+ int64_t service_worker_registration_id,
+ const std::string& sender_id,
+ const std::string& subscription_id,
+ SubscriptionInfoCallback callback) override;
+ void Unsubscribe(blink::mojom::PushUnregistrationReason reason,
+ const GURL& requesting_origin,
+ int64_t service_worker_registration_id,
+ const std::string& sender_id,
+ UnregisterCallback) override;
+ bool SupportNonVisibleMessages() override;
+ void DidDeleteServiceWorkerRegistration(
+ const GURL& origin,
+ int64_t service_worker_registration_id) override;
+ void DidDeleteServiceWorkerDatabase() override;
+
+ // content_settings::Observer implementation.
+ void OnContentSettingChanged(
+ const ContentSettingsPattern& primary_pattern,
+ const ContentSettingsPattern& secondary_pattern,
+ ContentSettingsTypeSet content_type_set) override;
+
+ // Fires the `pushsubscriptionchange` event to the associated service worker
+ // of |app_identifier|, which is the app identifier for |old_subscription|
+ // whereas |new_subscription| can be either null e.g. when a subscription is
+ // lost due to permission changes or a new subscription when it was refreshed.
+ void FirePushSubscriptionChange(
+ const PushMessagingAppIdentifier& app_identifier,
+ base::OnceClosure completed_closure,
+ blink::mojom::PushSubscriptionPtr new_subscription,
+ blink::mojom::PushSubscriptionPtr old_subscription);
+
+ // KeyedService implementation.
+ void Shutdown() override;
+
+ // content::NotificationObserver implementation
+ void Observe(int type,
+ const content::NotificationSource& source,
+ const content::NotificationDetails& details) override;
+
+ // WARNING: Only call this function if features::kPushSubscriptionChangeEvent
+ // is enabled, will be later used by the Push Service to trigger subscription
+ // refreshes
+ void OnSubscriptionInvalidation(const std::string& app_id);
+
+ // PushMessagingRefresher::Observer implementation
+ // Initiate unsubscribe task when old subscription becomes invalid
+ void OnOldSubscriptionExpired(const std::string& app_id,
+ const std::string& sender_id) override;
+ void OnRefreshFinished(
+ const PushMessagingAppIdentifier& app_identifier) override;
+
+ void SetMessageCallbackForTesting(const base::RepeatingClosure& callback);
+ void SetUnsubscribeCallbackForTesting(base::OnceClosure callback);
+ void SetInvalidationCallbackForTesting(base::OnceClosure callback);
+ void SetContentSettingChangedCallbackForTesting(
+ base::RepeatingClosure callback);
+ void SetServiceWorkerUnregisteredCallbackForTesting(
+ base::RepeatingClosure callback);
+ void SetServiceWorkerDatabaseWipedCallbackForTesting(
+ base::RepeatingClosure callback);
+ void SetRemoveExpiredSubscriptionsCallbackForTesting(
+ base::OnceClosure closure);
+
+ private:
+ friend class PushMessagingBrowserTestBase;
+ friend class PushMessagingServiceTest;
+ FRIEND_TEST_ALL_PREFIXES(PushMessagingServiceTest, NormalizeSenderInfo);
+ FRIEND_TEST_ALL_PREFIXES(PushMessagingServiceTest, PayloadEncryptionTest);
+ FRIEND_TEST_ALL_PREFIXES(PushMessagingServiceTest,
+ TestMultipleIncomingPushMessages);
+
+ // A subscription is pending until it has succeeded or failed.
+ void IncreasePushSubscriptionCount(int add, bool is_pending);
+ void DecreasePushSubscriptionCount(int subtract, bool was_pending);
+
+ // OnMessage methods ---------------------------------------------------------
+
+ void DeliverMessageCallback(const std::string& app_id,
+ const GURL& requesting_origin,
+ int64_t service_worker_registration_id,
+ const gcm::IncomingMessage& message,
+ bool did_enqueue_message,
+ blink::mojom::PushEventStatus status);
+
+ void DidHandleEnqueuedMessage(
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ base::OnceCallback<void(bool)> message_handled_callback,
+ bool did_show_generic_notification);
+
+ void DidHandleMessage(const std::string& app_id,
+ const std::string& push_message_id,
+ bool did_show_generic_notification);
+
+ void OnCheckedOriginForAbuse(
+ PendingMessage message,
+ AbusiveOriginPermissionRevocationRequest::Outcome outcome);
+
+ void DeliverNextQueuedMessageForServiceWorkerRegistration(
+ const GURL& origin,
+ int64_t service_worker_registration_id);
+
+ void CheckOriginForAbuseAndDispatchNextMessage();
+
+ // Subscribe methods ---------------------------------------------------------
+
+ void DoSubscribe(PushMessagingAppIdentifier app_identifier,
+ blink::mojom::PushSubscriptionOptionsPtr options,
+ RegisterCallback callback,
+ int render_process_id,
+ int render_frame_id,
+ ContentSetting permission_status);
+
+ void SubscribeEnd(RegisterCallback callback,
+ const std::string& subscription_id,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh,
+ const std::vector<uint8_t>& auth,
+ blink::mojom::PushRegistrationStatus status);
+
+ void SubscribeEndWithError(RegisterCallback callback,
+ blink::mojom::PushRegistrationStatus status);
+
+ void DidSubscribe(const PushMessagingAppIdentifier& app_identifier,
+ const std::string& sender_id,
+ RegisterCallback callback,
+ const std::string& subscription_id,
+ instance_id::InstanceID::Result result);
+
+ void DidSubscribeWithEncryptionInfo(
+ const PushMessagingAppIdentifier& app_identifier,
+ RegisterCallback callback,
+ const std::string& subscription_id,
+ const GURL& endpoint,
+ std::string p256dh,
+ std::string auth_secret);
+
+ // GetSubscriptionInfo methods -----------------------------------------------
+
+ void DidValidateSubscription(
+ const std::string& app_id,
+ const std::string& sender_id,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ SubscriptionInfoCallback callback,
+ bool is_valid);
+
+ void DidGetEncryptionInfo(const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ SubscriptionInfoCallback callback,
+ std::string p256dh,
+ std::string auth_secret) const;
+
+ // Unsubscribe methods -------------------------------------------------------
+
+ // |origin|, |service_worker_registration_id| and |app_id| should be provided
+ // whenever they can be obtained. It's valid for |origin| to be empty and
+ // |service_worker_registration_id| to be kInvalidServiceWorkerRegistrationId,
+ // or for app_id to be empty, but not both at once.
+ void UnsubscribeInternal(blink::mojom::PushUnregistrationReason reason,
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ const std::string& app_id,
+ const std::string& sender_id,
+ UnregisterCallback callback);
+
+ void DidClearPushSubscriptionId(blink::mojom::PushUnregistrationReason reason,
+ const std::string& app_id,
+ const std::string& sender_id,
+ UnregisterCallback callback);
+
+ void DidUnregister(bool was_subscribed, gcm::GCMClient::Result result);
+ void DidDeleteID(const std::string& app_id,
+ bool was_subscribed,
+ instance_id::InstanceID::Result result);
+ void DidUnsubscribe(const std::string& app_id_when_instance_id,
+ bool was_subscribed);
+
+ // OnContentSettingChanged methods -------------------------------------------
+
+ void GetPushSubscriptionFromAppIdentifier(
+ const PushMessagingAppIdentifier& app_identifier,
+ base::OnceCallback<void(blink::mojom::PushSubscriptionPtr)> callback);
+
+ void DidGetSWData(
+ const PushMessagingAppIdentifier& app_identifier,
+ base::OnceCallback<void(blink::mojom::PushSubscriptionPtr)> callback,
+ const std::string& sender_id,
+ const std::string& subscription_id);
+
+ void GetPushSubscriptionFromAppIdentifierEnd(
+ base::OnceCallback<void(blink::mojom::PushSubscriptionPtr)> callback,
+ const std::string& sender_id,
+ bool is_valid,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh,
+ const std::vector<uint8_t>& auth);
+
+ // OnSubscriptionInvalidation methods-----------------------------------------
+
+ void GetOldSubscription(PushMessagingAppIdentifier old_app_identifier,
+ const std::string& sender_id);
+
+ // After gathering all relavent information to start the refresh,
+ // generate a new app id and initiate refresh
+ void StartRefresh(PushMessagingAppIdentifier old_app_identifier,
+ const std::string& sender_id,
+ blink::mojom::PushSubscriptionPtr old_subscription);
+
+ // Makes a new susbcription and replaces the old subscription by new
+ // subscription in preferences and service worker database
+ void UpdateSubscription(PushMessagingAppIdentifier app_identifier,
+ blink::mojom::PushSubscriptionOptionsPtr options,
+ RegisterCallback callback);
+
+ // After the subscription is updated, fire a `pushsubscriptionchange` event
+ // and notify the |refresher_|
+ void DidUpdateSubscription(const std::string& new_app_id,
+ const std::string& old_app_id,
+ blink::mojom::PushSubscriptionPtr old_subscription,
+ const std::string& sender_id,
+ const std::string& registration_id,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh,
+ const std::vector<uint8_t>& auth,
+ blink::mojom::PushRegistrationStatus status);
+ // Helper methods ------------------------------------------------------------
+
+ // The subscription given in |identifier| will be unsubscribed (and a
+ // `pushsubscriptionchange` event fires if
+ // features::kPushSubscriptionChangeEvent is enabled)
+ void UnexpectedChange(PushMessagingAppIdentifier identifier,
+ blink::mojom::PushUnregistrationReason reason,
+ base::OnceClosure completed_closure);
+
+ void UnexpectedUnsubscribe(const PushMessagingAppIdentifier& app_identifier,
+ blink::mojom::PushUnregistrationReason reason,
+ UnregisterCallback unregister_callback);
+
+ void DidGetSenderIdUnexpectedUnsubscribe(
+ const PushMessagingAppIdentifier& app_identifier,
+ blink::mojom::PushUnregistrationReason reason,
+ UnregisterCallback callback,
+ const std::string& sender_id);
+
+ void FirePushSubscriptionChangeCallback(
+ const PushMessagingAppIdentifier& app_identifier,
+ blink::mojom::PushEventStatus status);
+
+ // Checks if a given origin is allowed to use Push.
+ bool IsPermissionSet(const GURL& origin, bool user_visible = true);
+
+ // Wrapper around {GCMDriver, InstanceID}::GetEncryptionInfo.
+ void GetEncryptionInfoForAppId(
+ const std::string& app_id,
+ const std::string& sender_id,
+ gcm::GCMEncryptionProvider::EncryptionInfoCallback callback);
+
+ gcm::GCMDriver* GetGCMDriver() const;
+
+ instance_id::InstanceIDDriver* GetInstanceIDDriver() const;
+
+ content::DevToolsBackgroundServicesContext* GetDevToolsContext(
+ const GURL& origin) const;
+
+ // Testing methods -----------------------------------------------------------
+
+ using PushEventCallback =
+ base::OnceCallback<void(blink::mojom::PushEventStatus)>;
+ using MessageDispatchedCallback =
+ base::RepeatingCallback<void(const std::string& app_id,
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ absl::optional<std::string> payload,
+ PushEventCallback callback)>;
+
+ // Callback to be invoked when a message has been dispatched. Enables tests to
+ // observe message delivery instead of delivering it to the Service Worker.
+ void SetMessageDispatchedCallbackForTesting(
+ const MessageDispatchedCallback& callback) {
+ message_dispatched_callback_for_testing_ = callback;
+ }
+
+ raw_ptr<Profile> profile_;
+ std::unique_ptr<AbusiveOriginPermissionRevocationRequest>
+ abusive_origin_revocation_request_;
+ std::queue<PendingMessage> messages_pending_permission_check_;
+
+ // {Origin, ServiceWokerRegistratonId} key for message delivery queue. This
+ // ensures that we only deliver one message at a time per ServiceWorker.
+ using MessageDeliveryQueueKey = std::pair<GURL, int64_t>;
+
+ // Queue of pending messages per ServiceWorkerRegstration to be delivered one
+ // at a time. This allows us to enforce visibility requirements.
+ base::flat_map<MessageDeliveryQueueKey, std::queue<PendingMessage>>
+ message_delivery_queue_;
+
+ int push_subscription_count_;
+ int pending_push_subscription_count_;
+
+ base::RepeatingClosure message_callback_for_testing_;
+ base::OnceClosure unsubscribe_callback_for_testing_;
+ base::RepeatingClosure content_setting_changed_callback_for_testing_;
+ base::RepeatingClosure service_worker_unregistered_callback_for_testing_;
+ base::RepeatingClosure service_worker_database_wiped_callback_for_testing_;
+ base::OnceClosure remove_expired_subscriptions_callback_for_testing_;
+ base::OnceClosure invalidation_callback_for_testing_;
+
+ PushMessagingNotificationManager notification_manager_;
+
+ PushMessagingRefresher refresher_;
+
+ base::ScopedObservation<PushMessagingRefresher,
+ PushMessagingRefresher::Observer>
+ refresh_observation_{this};
+
+ MessageDispatchedCallback message_dispatched_callback_for_testing_;
+
+#if BUILDFLAG(ENABLE_BACKGROUND_MODE)
+ // KeepAlive registered while we have in-flight push messages, to make sure
+ // we can finish processing them without being interrupted by BrowserProcess
+ // teardown.
+ std::unique_ptr<ScopedKeepAlive> in_flight_keep_alive_;
+
+ // Same as ScopedKeepAlive, but prevents |profile_| from getting deleted.
+ std::unique_ptr<ScopedProfileKeepAlive> in_flight_profile_keep_alive_;
+#endif
+
+ content::NotificationRegistrar registrar_;
+
+ // True when shutdown has started. Do not allow processing of incoming
+ // messages when this is true.
+ bool shutdown_started_ = false;
+
+ base::WeakPtrFactory<PushMessagingServiceImpl> weak_factory_{this};
+};
+
+#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_IMPL_H_
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_unittest.cc b/chromium/chrome/browser/push_messaging/push_messaging_service_unittest.cc
new file mode 100644
index 00000000000..612589a5107
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_service_unittest.cc
@@ -0,0 +1,480 @@
+// Copyright 2015 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.
+
+#include "content/public/browser/push_messaging_service.h"
+
+#include <stdint.h>
+
+#include <string>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/command_line.h"
+#include "base/cxx17_backports.h"
+#include "base/run_loop.h"
+#include "base/test/bind.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/time/time.h"
+#include "build/build_config.h"
+#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
+#include "chrome/browser/gcm/gcm_profile_service_factory.h"
+#include "chrome/browser/permissions/permission_manager_factory.h"
+#include "chrome/browser/push_messaging/push_messaging_app_identifier.h"
+#include "chrome/browser/push_messaging/push_messaging_features.h"
+#include "chrome/browser/push_messaging/push_messaging_service_factory.h"
+#include "chrome/browser/push_messaging/push_messaging_service_impl.h"
+#include "chrome/browser/push_messaging/push_messaging_utils.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/content_settings/core/browser/host_content_settings_map.h"
+#include "components/gcm_driver/crypto/gcm_crypto_test_helpers.h"
+#include "components/gcm_driver/fake_gcm_client_factory.h"
+#include "components/gcm_driver/fake_gcm_profile_service.h"
+#include "components/gcm_driver/gcm_profile_service.h"
+#include "components/permissions/permission_manager.h"
+#include "content/public/common/content_features.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h"
+
+#if defined(OS_ANDROID)
+#include "components/gcm_driver/instance_id/instance_id_android.h"
+#include "components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h"
+#endif // OS_ANDROID
+
+namespace {
+
+const char kTestOrigin[] = "https://example.com";
+const char kTestSenderId[] = "1234567890";
+const int64_t kTestServiceWorkerId = 42;
+const char kTestPayload[] = "Hello, world!";
+
+// NIST P-256 public key in uncompressed format per SEC1 2.3.3.
+const uint8_t kTestP256Key[] = {
+ 0x04, 0x55, 0x52, 0x6A, 0xA5, 0x6E, 0x8E, 0xAA, 0x47, 0x97, 0x36,
+ 0x10, 0xC1, 0x66, 0x3C, 0x1E, 0x65, 0xBF, 0xA1, 0x7B, 0xEE, 0x48,
+ 0xC9, 0xC6, 0xBB, 0xBF, 0x02, 0x18, 0x53, 0x72, 0x1D, 0x0C, 0x7B,
+ 0xA9, 0xE3, 0x11, 0xB7, 0x03, 0x52, 0x21, 0xD3, 0x71, 0x90, 0x13,
+ 0xA8, 0xC1, 0xCF, 0xED, 0x20, 0xF7, 0x1F, 0xD1, 0x7F, 0xF2, 0x76,
+ 0xB6, 0x01, 0x20, 0xD8, 0x35, 0xA5, 0xD9, 0x3C, 0x43, 0xFD};
+
+static_assert(sizeof(kTestP256Key) == 65,
+ "The fake public key must be a valid P-256 uncompressed point.");
+
+// URL-safe base64 encoded version of the |kTestP256Key|.
+const char kTestEncodedP256Key[] =
+ "BFVSaqVujqpHlzYQwWY8HmW_oXvuSMnGu78CGFNyHQx7qeMRtwNSIdNxkBOowc_tIPcf0X_ydr"
+ "YBINg1pdk8Q_0";
+
+// Implementation of the TestingProfile that provides the Push Messaging Service
+// and the Permission Manager, both of which are required for the tests.
+class PushMessagingTestingProfile : public TestingProfile {
+ public:
+ PushMessagingTestingProfile() = default;
+
+ PushMessagingTestingProfile(const PushMessagingTestingProfile&) = delete;
+ PushMessagingTestingProfile& operator=(const PushMessagingTestingProfile&) =
+ delete;
+
+ ~PushMessagingTestingProfile() override = default;
+
+ PushMessagingServiceImpl* GetPushMessagingService() override {
+ return PushMessagingServiceFactory::GetForProfile(this);
+ }
+
+ permissions::PermissionManager* GetPermissionControllerDelegate() override {
+ return PermissionManagerFactory::GetForProfile(this);
+ }
+};
+
+std::unique_ptr<KeyedService> BuildFakeGCMProfileService(
+ content::BrowserContext* context) {
+ return gcm::FakeGCMProfileService::Build(static_cast<Profile*>(context));
+}
+
+constexpr base::TimeDelta kPushEventHandleTime = base::Seconds(10);
+
+} // namespace
+
+class PushMessagingServiceTest : public ::testing::Test {
+ public:
+ PushMessagingServiceTest() {
+ // Always allow push notifications in the profile.
+ HostContentSettingsMap* host_content_settings_map =
+ HostContentSettingsMapFactory::GetForProfile(&profile_);
+ host_content_settings_map->SetDefaultContentSetting(
+ ContentSettingsType::NOTIFICATIONS, CONTENT_SETTING_ALLOW);
+
+ // Override the GCM Profile service so that we can send fake messages.
+ gcm::GCMProfileServiceFactory::GetInstance()->SetTestingFactory(
+ &profile_, base::BindRepeating(&BuildFakeGCMProfileService));
+ }
+
+ ~PushMessagingServiceTest() override = default;
+
+ // Callback to use when the subscription may have been subscribed.
+ void DidRegister(std::string* subscription_id_out,
+ GURL* endpoint_out,
+ absl::optional<base::Time>* expiration_time_out,
+ std::vector<uint8_t>* p256dh_out,
+ std::vector<uint8_t>* auth_out,
+ base::OnceClosure done_callback,
+ const std::string& registration_id,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh,
+ const std::vector<uint8_t>& auth,
+ blink::mojom::PushRegistrationStatus status) {
+ EXPECT_EQ(blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE,
+ status);
+
+ *subscription_id_out = registration_id;
+ *expiration_time_out = expiration_time;
+ *endpoint_out = endpoint;
+ *p256dh_out = p256dh;
+ *auth_out = auth;
+
+ std::move(done_callback).Run();
+ }
+
+ // Callback to use when observing messages dispatched by the push service.
+ void DidDispatchMessage(
+ std::string* app_id_out,
+ GURL* origin_out,
+ int64_t* service_worker_registration_id_out,
+ absl::optional<std::string>* payload_out,
+ const std::string& app_id,
+ const GURL& origin,
+ int64_t service_worker_registration_id,
+ absl::optional<std::string> payload,
+ PushMessagingServiceImpl::PushEventCallback callback) {
+ *app_id_out = app_id;
+ *origin_out = origin;
+ *service_worker_registration_id_out = service_worker_registration_id;
+ *payload_out = std::move(payload);
+ }
+
+ class TestPushSubscription {
+ public:
+ std::string subscription_id_;
+ GURL endpoint_;
+ absl::optional<base::Time> expiration_time_;
+ std::vector<uint8_t> p256dh_;
+ std::vector<uint8_t> auth_;
+ TestPushSubscription(const std::string& subscription_id,
+ const GURL& endpoint,
+ const absl::optional<base::Time>& expiration_time,
+ const std::vector<uint8_t>& p256dh,
+ const std::vector<uint8_t>& auth)
+ : subscription_id_(subscription_id),
+ endpoint_(endpoint),
+ expiration_time_(expiration_time),
+ p256dh_(p256dh),
+ auth_(auth) {}
+ TestPushSubscription() = default;
+ };
+
+ void Subscribe(PushMessagingServiceImpl* push_service,
+ const GURL& origin,
+ TestPushSubscription* subscription = nullptr) {
+ std::string subscription_id;
+ GURL endpoint;
+ absl::optional<base::Time> expiration_time;
+ std::vector<uint8_t> p256dh, auth;
+
+ base::RunLoop run_loop;
+
+ auto options = blink::mojom::PushSubscriptionOptions::New();
+ options->user_visible_only = true;
+ options->application_server_key = std::vector<uint8_t>(
+ kTestSenderId,
+ kTestSenderId + sizeof(kTestSenderId) / sizeof(char) - 1);
+
+ push_service->SubscribeFromWorker(
+ origin, kTestServiceWorkerId, std::move(options),
+ base::BindOnce(&PushMessagingServiceTest::DidRegister,
+ base::Unretained(this), &subscription_id, &endpoint,
+ &expiration_time, &p256dh, &auth,
+ run_loop.QuitClosure()));
+
+ EXPECT_EQ(0u, subscription_id.size()); // this must be asynchronous
+
+ run_loop.Run();
+
+ ASSERT_GT(subscription_id.size(), 0u);
+ ASSERT_TRUE(endpoint.is_valid());
+ ASSERT_GT(endpoint.spec().size(), 0u);
+ ASSERT_GT(p256dh.size(), 0u);
+ ASSERT_GT(auth.size(), 0u);
+
+ if (subscription) {
+ subscription->subscription_id_ = subscription_id;
+ subscription->endpoint_ = endpoint;
+ subscription->p256dh_ = p256dh;
+ subscription->auth_ = auth;
+ }
+ }
+
+ protected:
+ PushMessagingTestingProfile* profile() { return &profile_; }
+
+ content::BrowserTaskEnvironment& task_environment() {
+ return task_environment_;
+ }
+
+ private:
+ content::BrowserTaskEnvironment task_environment_{
+ base::test::TaskEnvironment::TimeSource::MOCK_TIME};
+ PushMessagingTestingProfile profile_;
+
+#if defined(OS_ANDROID)
+ instance_id::InstanceIDAndroid::ScopedBlockOnAsyncTasksForTesting
+ block_async_;
+#endif // OS_ANDROID
+};
+
+// Fails too often on Linux TSAN builder: http://crbug.com/1211350.
+#if defined(OS_LINUX) && defined(THREAD_SANITIZER)
+#define MAYBE_PayloadEncryptionTest DISABLED_PayloadEncryptionTest
+#else
+#define MAYBE_PayloadEncryptionTest PayloadEncryptionTest
+#endif
+TEST_F(PushMessagingServiceTest, MAYBE_PayloadEncryptionTest) {
+ PushMessagingServiceImpl* push_service = profile()->GetPushMessagingService();
+ ASSERT_TRUE(push_service);
+
+ const GURL origin(kTestOrigin);
+
+ // (1) Make sure that |kExampleOrigin| has access to use Push Messaging.
+ ASSERT_EQ(blink::mojom::PermissionStatus::GRANTED,
+ push_service->GetPermissionStatus(origin, true /* user_visible */));
+
+ // (2) Subscribe for Push Messaging, and verify that we've got the required
+ // information in order to be able to create encrypted messages.
+ TestPushSubscription subscription;
+ Subscribe(push_service, origin, &subscription);
+
+ // (3) Encrypt a message using the public key and authentication secret that
+ // are associated with the subscription.
+
+ gcm::IncomingMessage message;
+ message.sender_id = kTestSenderId;
+
+ ASSERT_TRUE(gcm::CreateEncryptedPayloadForTesting(
+ kTestPayload,
+ base::StringPiece(reinterpret_cast<char*>(subscription.p256dh_.data()),
+ subscription.p256dh_.size()),
+ base::StringPiece(reinterpret_cast<char*>(subscription.auth_.data()),
+ subscription.auth_.size()),
+ &message));
+
+ ASSERT_GT(message.raw_data.size(), 0u);
+ ASSERT_NE(kTestPayload, message.raw_data);
+ ASSERT_FALSE(message.decrypted);
+
+ // (4) Find the app_id that has been associated with the subscription.
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(profile(), origin,
+ kTestServiceWorkerId);
+
+ ASSERT_FALSE(app_identifier.is_null());
+
+ std::string app_id;
+ GURL dispatched_origin;
+ int64_t service_worker_registration_id;
+ absl::optional<std::string> payload;
+
+ // (5) Observe message dispatchings from the Push Messaging service, and
+ // then dispatch the |message| on the GCM driver as if it had actually
+ // been received by Google Cloud Messaging.
+ push_service->SetMessageDispatchedCallbackForTesting(base::BindRepeating(
+ &PushMessagingServiceTest::DidDispatchMessage, base::Unretained(this),
+ &app_id, &dispatched_origin, &service_worker_registration_id, &payload));
+
+ gcm::FakeGCMProfileService* fake_profile_service =
+ static_cast<gcm::FakeGCMProfileService*>(
+ gcm::GCMProfileServiceFactory::GetForProfile(profile()));
+
+ fake_profile_service->DispatchMessage(app_identifier.app_id(), message);
+
+ base::RunLoop().RunUntilIdle();
+
+ // (6) Verify that the message, as received by the Push Messaging Service, has
+ // indeed been decrypted by the GCM Driver, and has been forwarded to the
+ // Service Worker that has been associated with the subscription.
+ EXPECT_EQ(app_identifier.app_id(), app_id);
+ EXPECT_EQ(origin, dispatched_origin);
+ EXPECT_EQ(service_worker_registration_id, kTestServiceWorkerId);
+
+ EXPECT_TRUE(payload);
+ EXPECT_EQ(kTestPayload, *payload);
+}
+
+TEST_F(PushMessagingServiceTest, NormalizeSenderInfo) {
+ PushMessagingServiceImpl* push_service = profile()->GetPushMessagingService();
+ ASSERT_TRUE(push_service);
+
+ std::string p256dh(kTestP256Key, kTestP256Key + base::size(kTestP256Key));
+ ASSERT_EQ(65u, p256dh.size());
+
+ // NIST P-256 public keys in uncompressed format will be encoded using the
+ // URL-safe base64 encoding by the normalization function.
+ EXPECT_EQ(kTestEncodedP256Key, push_messaging::NormalizeSenderInfo(p256dh));
+
+ // Any other value, binary or not, will be passed through as-is.
+ EXPECT_EQ("1234567890", push_messaging::NormalizeSenderInfo("1234567890"));
+ EXPECT_EQ("foo@bar.com", push_messaging::NormalizeSenderInfo("foo@bar.com"));
+
+ p256dh[0] = 0x05; // invalidate |p256dh| as a public key.
+
+ EXPECT_EQ(p256dh, push_messaging::NormalizeSenderInfo(p256dh));
+}
+
+// Fails too often on Linux TSAN builder: http://crbug.com/1211350.
+#if defined(OS_LINUX) && defined(THREAD_SANITIZER)
+#define MAYBE_RemoveExpiredSubscriptions DISABLED_RemoveExpiredSubscriptions
+#else
+#define MAYBE_RemoveExpiredSubscriptions RemoveExpiredSubscriptions
+#endif
+TEST_F(PushMessagingServiceTest, MAYBE_RemoveExpiredSubscriptions) {
+ // (1) Enable push subscriptions with expiration time and
+ // `pushsubscriptionchange` events
+ base::test::ScopedFeatureList scoped_feature_list_;
+ scoped_feature_list_.InitWithFeatures(
+ /* enabled features */
+ {features::kPushSubscriptionWithExpirationTime,
+ features::kPushSubscriptionChangeEvent},
+ /* disabled features */
+ {});
+
+ // (2) Set up push service and test origin
+ PushMessagingServiceImpl* push_service = profile()->GetPushMessagingService();
+ ASSERT_TRUE(push_service);
+ const GURL origin(kTestOrigin);
+
+ // (3) Subscribe origin to push service and find corresponding
+ // |app_identifier|
+ Subscribe(push_service, origin);
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(profile(), origin,
+ kTestServiceWorkerId);
+ ASSERT_FALSE(app_identifier.is_null());
+
+ // (4) Manually set the time as expired, save the time in preferences
+ app_identifier.set_expiration_time(base::Time::UnixEpoch());
+ app_identifier.PersistToPrefs(profile());
+ ASSERT_EQ(1u, PushMessagingAppIdentifier::GetCount(profile()));
+
+ // (3) Remove all expired subscriptions
+ base::RunLoop run_loop;
+ push_service->SetRemoveExpiredSubscriptionsCallbackForTesting(
+ run_loop.QuitClosure());
+ push_service->RemoveExpiredSubscriptions();
+ run_loop.Run();
+
+ // (5) We expect the subscription to be deleted
+ ASSERT_EQ(0u, PushMessagingAppIdentifier::GetCount(profile()));
+ PushMessagingAppIdentifier deleted_identifier =
+ PushMessagingAppIdentifier::FindByAppId(profile(),
+ app_identifier.app_id());
+ EXPECT_TRUE(deleted_identifier.is_null());
+}
+
+TEST_F(PushMessagingServiceTest, TestMultipleIncomingPushMessages) {
+ base::HistogramTester histograms;
+ PushMessagingServiceImpl* push_service = profile()->GetPushMessagingService();
+ ASSERT_TRUE(push_service);
+
+ // Subscribe |origin| to push service.
+ const GURL origin(kTestOrigin);
+ Subscribe(push_service, origin);
+ PushMessagingAppIdentifier app_identifier =
+ PushMessagingAppIdentifier::FindByServiceWorker(profile(), origin,
+ kTestServiceWorkerId);
+ ASSERT_FALSE(app_identifier.is_null());
+
+ // Setup decrypted test message.
+ gcm::IncomingMessage message;
+ message.sender_id = kTestSenderId;
+ message.raw_data = "testdata";
+ message.decrypted = true;
+
+ // Setup callbacks for dispatch and handled push events.
+ auto dispatched_run_loop = std::make_unique<base::RunLoop>();
+ auto handled_run_loop = std::make_unique<base::RunLoop>();
+ PushMessagingServiceImpl::PushEventCallback handle_push_event;
+
+ push_service->SetMessageDispatchedCallbackForTesting(
+ base::BindLambdaForTesting(
+ [&](const std::string& app_id, const GURL& origin,
+ int64_t service_worker_registration_id,
+ absl::optional<std::string> payload,
+ PushMessagingServiceImpl::PushEventCallback callback) {
+ handle_push_event = std::move(callback);
+ dispatched_run_loop->Quit();
+ }));
+
+ push_service->SetMessageCallbackForTesting(
+ base::BindLambdaForTesting([&]() { handled_run_loop->Quit(); }));
+
+ // Simulate two incoming push messages at the same time.
+ push_service->OnMessage(app_identifier.app_id(), message);
+ push_service->OnMessage(app_identifier.app_id(), message);
+
+ // First wait until we dispatched the first push message.
+ dispatched_run_loop->Run();
+ dispatched_run_loop = std::make_unique<base::RunLoop>();
+ auto handled_first = std::move(handle_push_event);
+ handle_push_event = PushMessagingServiceImpl::PushEventCallback();
+
+ histograms.ExpectUniqueTimeSample("PushMessaging.CheckOriginForAbuseTime",
+ base::Seconds(0),
+ /*expected_bucket_count=*/1);
+ histograms.ExpectUniqueTimeSample("PushMessaging.DeliverQueuedMessageTime",
+ base::Seconds(0),
+ /*expected_bucket_count=*/1);
+
+ // Run all tasks until idle so we can verify that we don't dispatch the second
+ // push message until the first one is handled.
+ base::RunLoop().RunUntilIdle();
+ EXPECT_FALSE(handle_push_event);
+
+ // Simulate handling the first push event takes some time.
+ task_environment().FastForwardBy(kPushEventHandleTime);
+
+ // Now signal that the first push event has been handled and wait until we
+ // checked for visibility requirements.
+ std::move(handled_first).Run(blink::mojom::PushEventStatus::SUCCESS);
+ handled_run_loop->Run();
+ handled_run_loop = std::make_unique<base::RunLoop>();
+
+ histograms.ExpectUniqueTimeSample("PushMessaging.MessageHandledTime",
+ kPushEventHandleTime,
+ /*expected_bucket_count=*/1);
+
+ // Simulate handling the second push event takes some time.
+ task_environment().FastForwardBy(kPushEventHandleTime);
+
+ // Now wait until we dispatched the second push message and handle it too.
+ dispatched_run_loop->Run();
+ std::move(handle_push_event).Run(blink::mojom::PushEventStatus::SUCCESS);
+ handled_run_loop->Run();
+
+ // Checking origins for abuse happens immediately on receiving a push message
+ // one at a time. Both messages do that instantly in this test.
+ histograms.ExpectTimeBucketCount("PushMessaging.CheckOriginForAbuseTime",
+ base::Seconds(0),
+ /*count=*/2);
+ // Delivering messages should be done in series so the second message should
+ // have waited for the first one to be handled.
+ histograms.ExpectTimeBucketCount("PushMessaging.DeliverQueuedMessageTime",
+ kPushEventHandleTime,
+ /*count=*/1);
+ // The total time from receiving until handling of the second message.
+ histograms.ExpectTimeBucketCount("PushMessaging.MessageHandledTime",
+ kPushEventHandleTime * 2,
+ /*count=*/1);
+}
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_utils.cc b/chromium/chrome/browser/push_messaging/push_messaging_utils.cc
new file mode 100644
index 00000000000..b54bf14acf3
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_utils.cc
@@ -0,0 +1,44 @@
+// Copyright 2020 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.
+
+#include "chrome/browser/push_messaging/push_messaging_utils.h"
+#include "base/base64url.h"
+#include "chrome/browser/push_messaging/push_messaging_constants.h"
+#include "url/gurl.h"
+
+namespace push_messaging {
+
+GURL CreateEndpoint(const std::string& subscription_id) {
+ const GURL endpoint(kPushMessagingGcmEndpoint + subscription_id);
+ DCHECK(endpoint.is_valid());
+ return endpoint;
+}
+
+blink::mojom::PushSubscriptionOptionsPtr MakeOptions(
+ const std::string& sender_id) {
+ return blink::mojom::PushSubscriptionOptions::New(
+ /*user_visible_only=*/true,
+ std::vector<uint8_t>(sender_id.begin(), sender_id.end()));
+}
+
+bool IsVapidKey(const std::string& application_server_key) {
+ // VAPID keys are NIST P-256 public keys in uncompressed format (64 bytes),
+ // verified through its length and the 0x04 prefix.
+ return application_server_key.size() == 65 &&
+ application_server_key[0] == 0x04;
+}
+
+std::string NormalizeSenderInfo(const std::string& application_server_key) {
+ if (!IsVapidKey(application_server_key))
+ return application_server_key;
+
+ std::string encoded_application_server_key;
+ base::Base64UrlEncode(application_server_key,
+ base::Base64UrlEncodePolicy::OMIT_PADDING,
+ &encoded_application_server_key);
+
+ return encoded_application_server_key;
+}
+
+} // namespace push_messaging
diff --git a/chromium/chrome/browser/push_messaging/push_messaging_utils.h b/chromium/chrome/browser/push_messaging/push_messaging_utils.h
new file mode 100644
index 00000000000..805deeab47e
--- /dev/null
+++ b/chromium/chrome/browser/push_messaging/push_messaging_utils.h
@@ -0,0 +1,34 @@
+// Copyright 2020 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_PUSH_MESSAGING_PUSH_MESSAGING_UTILS_H_
+#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_UTILS_H_
+
+#include <string>
+#include "third_party/blink/public/mojom/push_messaging/push_messaging.mojom.h"
+
+class GURL;
+
+namespace push_messaging {
+
+// Returns the URL used to send push messages to the subscription identified
+// by |subscription_id|.
+GURL CreateEndpoint(const std::string& subscription_id);
+
+// Checks size and prefix to determine whether it is a VAPID key
+bool IsVapidKey(const std::string& application_server_key);
+
+// Normalizes the |sender_info|. In most cases the |sender_info| will be
+// passed through to the GCM Driver as-is, but NIST P-256 application server
+// keys have to be encoded using the URL-safe variant of the base64 encoding.
+std::string NormalizeSenderInfo(const std::string& sender_info);
+
+// Currently |user_visible_only| is always true, once silent pushes are
+// enabled, get this information from SW database.
+blink::mojom::PushSubscriptionOptionsPtr MakeOptions(
+ const std::string& sender_id);
+
+} // namespace push_messaging
+
+#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_UTILS_H_
diff --git a/chromium/chrome/browser/signin/DEPS b/chromium/chrome/browser/signin/DEPS
new file mode 100644
index 00000000000..e2cc84fd222
--- /dev/null
+++ b/chromium/chrome/browser/signin/DEPS
@@ -0,0 +1,3 @@
+include_rules = [
+ "+ash/components/account_manager",
+]
diff --git a/chromium/chrome/browser/signin/DIR_METADATA b/chromium/chrome/browser/signin/DIR_METADATA
new file mode 100644
index 00000000000..95ad5b66d16
--- /dev/null
+++ b/chromium/chrome/browser/signin/DIR_METADATA
@@ -0,0 +1 @@
+mixins: "//components/signin/COMMON_METADATA"
diff --git a/chromium/chrome/browser/signin/OWNERS b/chromium/chrome/browser/signin/OWNERS
new file mode 100644
index 00000000000..017d5a003db
--- /dev/null
+++ b/chromium/chrome/browser/signin/OWNERS
@@ -0,0 +1,5 @@
+file://components/signin/OWNERS
+
+per-file chrome_proximity_auth_*=xiyuan@chromium.org
+per-file easy_unlock_*=xiyuan@chromium.org
+per-file signin_profile_attributes_updater*=msalama@chromium.org
diff --git a/chromium/chrome/browser/signin/about_signin_internals_factory.cc b/chromium/chrome/browser/signin/about_signin_internals_factory.cc
new file mode 100644
index 00000000000..7e62dc93ebf
--- /dev/null
+++ b/chromium/chrome/browser/signin/about_signin_internals_factory.cc
@@ -0,0 +1,58 @@
+// Copyright (c) 2012 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.
+
+#include "chrome/browser/signin/about_signin_internals_factory.h"
+
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/account_consistency_mode_manager_factory.h"
+#include "chrome/browser/signin/account_reconcilor_factory.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_error_controller_factory.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/signin/core/browser/about_signin_internals.h"
+
+AboutSigninInternalsFactory::AboutSigninInternalsFactory()
+ : BrowserContextKeyedServiceFactory(
+ "AboutSigninInternals",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(ChromeSigninClientFactory::GetInstance());
+ DependsOn(SigninErrorControllerFactory::GetInstance());
+ DependsOn(AccountReconcilorFactory::GetInstance());
+ DependsOn(IdentityManagerFactory::GetInstance());
+ DependsOn(AccountConsistencyModeManagerFactory::GetInstance());
+}
+
+AboutSigninInternalsFactory::~AboutSigninInternalsFactory() {}
+
+// static
+AboutSigninInternals* AboutSigninInternalsFactory::GetForProfile(
+ Profile* profile) {
+ return static_cast<AboutSigninInternals*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+AboutSigninInternalsFactory* AboutSigninInternalsFactory::GetInstance() {
+ return base::Singleton<AboutSigninInternalsFactory>::get();
+}
+
+void AboutSigninInternalsFactory::RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* user_prefs) {
+ AboutSigninInternals::RegisterPrefs(user_prefs);
+}
+
+KeyedService* AboutSigninInternalsFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ AboutSigninInternals* service = new AboutSigninInternals(
+ IdentityManagerFactory::GetForProfile(profile),
+ SigninErrorControllerFactory::GetForProfile(profile),
+ AccountConsistencyModeManager::GetMethodForProfile(profile),
+ ChromeSigninClientFactory::GetForProfile(profile),
+ AccountReconcilorFactory::GetForProfile(profile));
+ return service;
+}
diff --git a/chromium/chrome/browser/signin/about_signin_internals_factory.h b/chromium/chrome/browser/signin/about_signin_internals_factory.h
new file mode 100644
index 00000000000..c1b7cb5a71d
--- /dev/null
+++ b/chromium/chrome/browser/signin/about_signin_internals_factory.h
@@ -0,0 +1,40 @@
+// Copyright (c) 2012 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_SIGNIN_ABOUT_SIGNIN_INTERNALS_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_ABOUT_SIGNIN_INTERNALS_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class AboutSigninInternals;
+class Profile;
+
+// Singleton that owns all AboutSigninInternals and associates them with
+// Profiles.
+class AboutSigninInternalsFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ // Returns the instance of AboutSigninInternals associated with this profile,
+ // creating one if none exists.
+ static AboutSigninInternals* GetForProfile(Profile* profile);
+
+ // Returns an instance of the AboutSigninInternalsFactory singleton.
+ static AboutSigninInternalsFactory* GetInstance();
+
+ // Implementation of BrowserContextKeyedServiceFactory.
+ void RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) override;
+
+ private:
+ friend struct base::DefaultSingletonTraits<AboutSigninInternalsFactory>;
+
+ AboutSigninInternalsFactory();
+ ~AboutSigninInternalsFactory() override;
+
+ // BrowserContextKeyedServiceFactory
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* context) const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_ABOUT_SIGNIN_INTERNALS_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager.cc b/chromium/chrome/browser/signin/account_consistency_mode_manager.cc
new file mode 100644
index 00000000000..6c1f0f04720
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_consistency_mode_manager.cc
@@ -0,0 +1,208 @@
+// Copyright 2017 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.
+
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+
+#include <string>
+
+#include "base/command_line.h"
+#include "base/logging.h"
+#include "base/metrics/field_trial_params.h"
+#include "base/metrics/histogram_macros.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profiles_state.h"
+#include "chrome/browser/signin/account_consistency_mode_manager_factory.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/common/pref_names.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/signin_buildflags.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "google_apis/google_api_keys.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/ash/account_manager/account_manager_util.h"
+#endif
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT) && BUILDFLAG(ENABLE_MIRROR)
+#error "Dice and Mirror cannot be both enabled."
+#endif
+
+#if !BUILDFLAG(ENABLE_DICE_SUPPORT) && !BUILDFLAG(ENABLE_MIRROR)
+#error "Either Dice or Mirror should be enabled."
+#endif
+
+using signin::AccountConsistencyMethod;
+
+namespace {
+
+// By default, DICE is not enabled in builds lacking an API key. May be set to
+// true for tests.
+bool g_ignore_missing_oauth_client_for_testing = false;
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+const char kAllowBrowserSigninArgument[] = "allow-browser-signin";
+
+bool IsBrowserSigninAllowedByCommandLine() {
+ base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
+ if (command_line->HasSwitch(kAllowBrowserSigninArgument)) {
+ std::string allowBrowserSignin =
+ command_line->GetSwitchValueASCII(kAllowBrowserSigninArgument);
+ return base::ToLowerASCII(allowBrowserSignin) == "true";
+ }
+ // If the commandline flag is not provided, the default is true.
+ return true;
+}
+
+// Returns true if Desktop Identity Consistency can be enabled for this build
+// (i.e. if OAuth client ID and client secret are configured).
+bool CanEnableDiceForBuild() {
+ if (g_ignore_missing_oauth_client_for_testing ||
+ google_apis::HasOAuthClientConfigured()) {
+ return true;
+ }
+
+ // Only log this once.
+ static bool logged_warning = []() {
+ LOG(WARNING) << "Desktop Identity Consistency cannot be enabled as no "
+ "OAuth client ID and client secret have been configured.";
+ return true;
+ }();
+ ALLOW_UNUSED_LOCAL(logged_warning);
+
+ return false;
+}
+#endif
+
+} // namespace
+
+// static
+AccountConsistencyModeManager* AccountConsistencyModeManager::GetForProfile(
+ Profile* profile) {
+ return AccountConsistencyModeManagerFactory::GetForProfile(profile);
+}
+
+AccountConsistencyModeManager::AccountConsistencyModeManager(Profile* profile)
+ : profile_(profile),
+ account_consistency_(signin::AccountConsistencyMethod::kDisabled),
+ account_consistency_initialized_(false) {
+ DCHECK(profile_);
+ DCHECK(ShouldBuildServiceForProfile(profile));
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ PrefService* prefs = profile->GetPrefs();
+ // Propagate settings changes from the previous launch to the signin-allowed
+ // pref.
+ bool signin_allowed = IsDiceSignInAllowed() &&
+ prefs->GetBoolean(prefs::kSigninAllowedOnNextStartup);
+ prefs->SetBoolean(prefs::kSigninAllowed, signin_allowed);
+
+ UMA_HISTOGRAM_BOOLEAN("Signin.SigninAllowed", signin_allowed);
+#endif
+
+ account_consistency_ = ComputeAccountConsistencyMethod(profile_);
+ DCHECK_EQ(account_consistency_, ComputeAccountConsistencyMethod(profile_));
+ account_consistency_initialized_ = true;
+}
+
+AccountConsistencyModeManager::~AccountConsistencyModeManager() {}
+
+// static
+void AccountConsistencyModeManager::RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) {
+ registry->RegisterBooleanPref(prefs::kSigninAllowedOnNextStartup, true);
+}
+
+// static
+AccountConsistencyMethod AccountConsistencyModeManager::GetMethodForProfile(
+ Profile* profile) {
+ if (!ShouldBuildServiceForProfile(profile))
+ return AccountConsistencyMethod::kDisabled;
+
+ return AccountConsistencyModeManager::GetForProfile(profile)
+ ->GetAccountConsistencyMethod();
+}
+
+// static
+bool AccountConsistencyModeManager::IsDiceEnabledForProfile(Profile* profile) {
+ return GetMethodForProfile(profile) == AccountConsistencyMethod::kDice;
+}
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+// static
+bool AccountConsistencyModeManager::IsDiceSignInAllowed() {
+ return CanEnableDiceForBuild() && IsBrowserSigninAllowedByCommandLine();
+}
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+// static
+bool AccountConsistencyModeManager::IsMirrorEnabledForProfile(
+ Profile* profile) {
+ return GetMethodForProfile(profile) == AccountConsistencyMethod::kMirror;
+}
+
+// static
+void AccountConsistencyModeManager::SetIgnoreMissingOAuthClientForTesting() {
+ g_ignore_missing_oauth_client_for_testing = true;
+}
+
+// static
+bool AccountConsistencyModeManager::ShouldBuildServiceForProfile(
+ Profile* profile) {
+ return profile->IsRegularProfile();
+}
+
+AccountConsistencyMethod
+AccountConsistencyModeManager::GetAccountConsistencyMethod() {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ // TODO(https://crbug.com/860671): ChromeOS should use the cached value.
+ // Changing the value dynamically is not supported.
+ return ComputeAccountConsistencyMethod(profile_);
+#else
+ // The account consistency method should not change during the lifetime of a
+ // profile. We always return the cached value, but still check that it did not
+ // change, in order to detect inconsisent states. See https://crbug.com/860471
+ CHECK(account_consistency_initialized_);
+ CHECK_EQ(ComputeAccountConsistencyMethod(profile_), account_consistency_);
+ return account_consistency_;
+#endif
+}
+
+// static
+signin::AccountConsistencyMethod
+AccountConsistencyModeManager::ComputeAccountConsistencyMethod(
+ Profile* profile) {
+ DCHECK(ShouldBuildServiceForProfile(profile));
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ if (!ash::IsAccountManagerAvailable(profile))
+ return AccountConsistencyMethod::kDisabled;
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+ // Account consistency is unavailable on Managed Guest Sessions and Public
+ // Sessions.
+ if (profiles::IsPublicSession())
+ return AccountConsistencyMethod::kDisabled;
+#endif
+
+#if BUILDFLAG(ENABLE_MIRROR)
+ return AccountConsistencyMethod::kMirror;
+#endif
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ if (!profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed)) {
+ VLOG(1) << "Desktop Identity Consistency disabled as sign-in to Chrome"
+ "is not allowed";
+ return AccountConsistencyMethod::kDisabled;
+ }
+
+ return AccountConsistencyMethod::kDice;
+#endif
+
+ NOTREACHED();
+ return AccountConsistencyMethod::kDisabled;
+}
diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager.h b/chromium/chrome/browser/signin/account_consistency_mode_manager.h
new file mode 100644
index 00000000000..8ca66aae9b9
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_consistency_mode_manager.h
@@ -0,0 +1,97 @@
+// Copyright 2017 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_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_H_
+#define CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_H_
+
+#include "base/feature_list.h"
+#include "base/gtest_prod_util.h"
+#include "base/memory/raw_ptr.h"
+#include "build/buildflag.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "components/prefs/pref_member.h"
+#include "components/signin/public/base/account_consistency_method.h"
+#include "components/signin/public/base/signin_buildflags.h"
+
+namespace user_prefs {
+class PrefRegistrySyncable;
+}
+
+class Profile;
+
+// Manages the account consistency mode for each profile.
+class AccountConsistencyModeManager : public KeyedService {
+ public:
+ // Returns the AccountConsistencyModeManager associated with this profile.
+ // May return nullptr if there is none (e.g. in incognito).
+ static AccountConsistencyModeManager* GetForProfile(Profile* profile);
+
+ explicit AccountConsistencyModeManager(Profile* profile);
+
+ AccountConsistencyModeManager(const AccountConsistencyModeManager&) = delete;
+ AccountConsistencyModeManager& operator=(
+ const AccountConsistencyModeManager&) = delete;
+
+ ~AccountConsistencyModeManager() override;
+
+ static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);
+
+ // Helper method, shorthand for calling GetAccountConsistencyMethod().
+ // TODO(crbug.com/1232361): Migrate usages to
+ // `IdentityManager::GetAccountConsistency`.
+ static signin::AccountConsistencyMethod GetMethodForProfile(Profile* profile);
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ // This is a pre-requisite of IsDiceEnabledForProfile(), independent of
+ // particular profile type or profile prefs.
+ static bool IsDiceSignInAllowed();
+#endif
+
+ // If true, then account management is done through Gaia webpages.
+ // Can only be used on the UI thread.
+ // Returns false if |profile| is in Guest or Incognito mode.
+ // A given |profile| will have only one of Mirror or Dice consistency
+ // behaviour enabled.
+ static bool IsDiceEnabledForProfile(Profile* profile);
+
+ // Returns |true| if Mirror account consistency is enabled for |profile|.
+ // Can only be used on the UI thread.
+ // A given |profile| will have only one of Mirror or Dice consistency
+ // behaviour enabled.
+ static bool IsMirrorEnabledForProfile(Profile* profile);
+
+ // By default, Desktop Identity Consistency (aka Dice) is not enabled in
+ // builds lacking an API key. For testing, set to have Dice enabled in tests.
+ static void SetIgnoreMissingOAuthClientForTesting();
+
+ // Returns true is the AccountConsistencyModeManager should be instantiated
+ // for the profile. Guest, incognito and system sessions do not instantiate
+ // the service.
+ static bool ShouldBuildServiceForProfile(Profile* profile);
+
+ private:
+ FRIEND_TEST_ALL_PREFIXES(AccountConsistencyModeManagerTest,
+ MigrateAtCreation);
+ FRIEND_TEST_ALL_PREFIXES(AccountConsistencyModeManagerTest,
+ SigninAllowedChangesDiceState);
+ FRIEND_TEST_ALL_PREFIXES(AccountConsistencyModeManagerTest,
+ AllowBrowserSigninSwitch);
+ FRIEND_TEST_ALL_PREFIXES(AccountConsistencyModeManagerTest,
+ DiceEnabledForNewProfiles);
+
+ // Returns the account consistency method for the current profile.
+ signin::AccountConsistencyMethod GetAccountConsistencyMethod();
+
+ // Computes the account consistency method for the current profile. This is
+ // only called from the constructor, the account consistency method cannot
+ // change during the lifetime of a profile.
+ static signin::AccountConsistencyMethod ComputeAccountConsistencyMethod(
+ Profile* profile);
+
+ raw_ptr<Profile> profile_;
+ signin::AccountConsistencyMethod account_consistency_;
+ bool account_consistency_initialized_;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_H_
diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.cc b/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.cc
new file mode 100644
index 00000000000..10178f34732
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.cc
@@ -0,0 +1,53 @@
+// Copyright 2018 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.
+
+#include "chrome/browser/signin/account_consistency_mode_manager_factory.h"
+
+#include "base/check.h"
+#include "build/build_config.h"
+#include "chrome/browser/profiles/profile.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+// static
+AccountConsistencyModeManagerFactory*
+AccountConsistencyModeManagerFactory::GetInstance() {
+ return base::Singleton<AccountConsistencyModeManagerFactory>::get();
+}
+
+// static
+AccountConsistencyModeManager*
+AccountConsistencyModeManagerFactory::GetForProfile(Profile* profile) {
+ DCHECK(profile);
+ return static_cast<AccountConsistencyModeManager*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+AccountConsistencyModeManagerFactory::AccountConsistencyModeManagerFactory()
+ : BrowserContextKeyedServiceFactory(
+ "AccountConsistencyModeManager",
+ BrowserContextDependencyManager::GetInstance()) {}
+
+AccountConsistencyModeManagerFactory::~AccountConsistencyModeManagerFactory() =
+ default;
+
+KeyedService* AccountConsistencyModeManagerFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ DCHECK(!context->IsOffTheRecord());
+ Profile* profile = Profile::FromBrowserContext(context);
+
+ return AccountConsistencyModeManager::ShouldBuildServiceForProfile(profile)
+ ? new AccountConsistencyModeManager(profile)
+ : nullptr;
+}
+
+void AccountConsistencyModeManagerFactory::RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) {
+ AccountConsistencyModeManager::RegisterProfilePrefs(registry);
+}
+
+bool AccountConsistencyModeManagerFactory::ServiceIsCreatedWithBrowserContext()
+ const {
+ return true;
+}
diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.h b/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.h
new file mode 100644
index 00000000000..7990d84bad5
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.h
@@ -0,0 +1,35 @@
+// Copyright 2018 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_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class AccountConsistencyModeManagerFactory
+ : public BrowserContextKeyedServiceFactory {
+ public:
+ // Returns an instance of the factory singleton.
+ static AccountConsistencyModeManagerFactory* GetInstance();
+
+ static AccountConsistencyModeManager* GetForProfile(Profile* profile);
+
+ private:
+ friend struct base::DefaultSingletonTraits<
+ AccountConsistencyModeManagerFactory>;
+
+ AccountConsistencyModeManagerFactory();
+ ~AccountConsistencyModeManagerFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* context) const override;
+ void RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) override;
+ bool ServiceIsCreatedWithBrowserContext() const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager_unittest.cc b/chromium/chrome/browser/signin/account_consistency_mode_manager_unittest.cc
new file mode 100644
index 00000000000..d0a4ee97b83
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_consistency_mode_manager_unittest.cc
@@ -0,0 +1,259 @@
+// Copyright 2017 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.
+
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/command_line.h"
+#include "base/test/scoped_command_line.h"
+#include "build/build_config.h"
+#include "build/buildflag.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/prefs/browser_prefs.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/prefs/pref_notifier_impl.h"
+#include "components/prefs/testing_pref_store.h"
+#include "components/signin/public/base/account_consistency_method.h"
+#include "components/signin/public/base/signin_buildflags.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/sync_preferences/testing_pref_service_syncable.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+std::unique_ptr<TestingProfile> BuildTestingProfile(bool is_new_profile) {
+ TestingProfile::Builder profile_builder;
+ profile_builder.SetIsNewProfile(is_new_profile);
+ std::unique_ptr<TestingProfile> profile = profile_builder.Build();
+ EXPECT_EQ(is_new_profile, profile->IsNewProfile());
+ return profile;
+}
+
+} // namespace
+
+// Check the default account consistency method.
+TEST(AccountConsistencyModeManagerTest, DefaultValue) {
+ content::BrowserTaskEnvironment task_environment;
+ std::unique_ptr<TestingProfile> profile =
+ BuildTestingProfile(/*is_new_profile=*/false);
+
+ signin::AccountConsistencyMethod method =
+#if BUILDFLAG(ENABLE_MIRROR)
+ signin::AccountConsistencyMethod::kMirror;
+#elif BUILDFLAG(ENABLE_DICE_SUPPORT)
+ signin::AccountConsistencyMethod::kDice;
+#else
+#error Either Dice or Mirror should be enabled
+#endif
+
+ EXPECT_EQ(method,
+ AccountConsistencyModeManager::GetMethodForProfile(profile.get()));
+ EXPECT_EQ(
+ method == signin::AccountConsistencyMethod::kMirror,
+ AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile.get()));
+ EXPECT_EQ(
+ method == signin::AccountConsistencyMethod::kDice,
+ AccountConsistencyModeManager::IsDiceEnabledForProfile(profile.get()));
+}
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+// Checks that changing the signin-allowed pref changes the Dice state on next
+// startup.
+TEST(AccountConsistencyModeManagerTest, SigninAllowedChangesDiceState) {
+ content::BrowserTaskEnvironment task_environment;
+ std::unique_ptr<TestingProfile> profile =
+ BuildTestingProfile(/*is_new_profile=*/false);
+
+ {
+ // First startup.
+ AccountConsistencyModeManager manager(profile.get());
+ EXPECT_TRUE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+ EXPECT_TRUE(
+ profile->GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup));
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDice,
+ manager.GetAccountConsistencyMethod());
+
+ // User changes their settings.
+ profile->GetPrefs()->SetBoolean(prefs::kSigninAllowedOnNextStartup, false);
+ // Dice should remain in the same state until restart.
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDice,
+ manager.GetAccountConsistencyMethod());
+ }
+
+ {
+ // Second startup.
+ AccountConsistencyModeManager manager(profile.get());
+ // The signin-allowed pref should be disabled.
+ EXPECT_FALSE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+ EXPECT_FALSE(
+ profile->GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup));
+ // Dice should be disabled.
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled,
+ manager.GetAccountConsistencyMethod());
+ }
+}
+
+TEST(AccountConsistencyModeManagerTest, AllowBrowserSigninSwitch) {
+ content::BrowserTaskEnvironment task_environment;
+ std::unique_ptr<TestingProfile> profile =
+ BuildTestingProfile(/*is_new_profile=*/false);
+ {
+ base::test::ScopedCommandLine scoped_command_line;
+ scoped_command_line.GetProcessCommandLine()->AppendSwitchASCII(
+ "allow-browser-signin", "false");
+ AccountConsistencyModeManager manager(profile.get());
+ EXPECT_FALSE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+ // Dice should be disabled.
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled,
+ manager.GetAccountConsistencyMethod());
+ }
+
+ {
+ base::test::ScopedCommandLine scoped_command_line;
+ scoped_command_line.GetProcessCommandLine()->AppendSwitchASCII(
+ "allow-browser-signin", "true");
+ AccountConsistencyModeManager manager(profile.get());
+ EXPECT_TRUE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+ // Dice should be enabled.
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDice,
+ manager.GetAccountConsistencyMethod());
+ }
+
+ {
+ AccountConsistencyModeManager manager(profile.get());
+ EXPECT_TRUE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+ EXPECT_TRUE(
+ profile->GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup));
+ // Dice should be enabled.
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDice,
+ manager.GetAccountConsistencyMethod());
+ }
+}
+
+// Checks that Dice is enabled for new profiles.
+TEST(AccountConsistencyModeManagerTest, DiceEnabledForNewProfiles) {
+ content::BrowserTaskEnvironment task_environment;
+ std::unique_ptr<TestingProfile> profile =
+ BuildTestingProfile(/*is_new_profile=*/false);
+ AccountConsistencyModeManager manager(profile.get());
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDice,
+ manager.GetAccountConsistencyMethod());
+}
+
+TEST(AccountConsistencyModeManagerTest, DiceOnlyForRegularProfile) {
+ content::BrowserTaskEnvironment task_environment;
+
+ {
+ // Regular profile.
+ TestingProfile profile;
+ EXPECT_TRUE(
+ AccountConsistencyModeManager::IsDiceEnabledForProfile(&profile));
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDice,
+ AccountConsistencyModeManager::GetMethodForProfile(&profile));
+ EXPECT_TRUE(
+ AccountConsistencyModeManager::ShouldBuildServiceForProfile(&profile));
+
+ // Incognito profile.
+ Profile* incognito_profile =
+ profile.GetPrimaryOTRProfile(/*create_if_needed=*/true);
+ EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile(
+ incognito_profile));
+ EXPECT_FALSE(
+ AccountConsistencyModeManager::GetForProfile(incognito_profile));
+ EXPECT_EQ(
+ signin::AccountConsistencyMethod::kDisabled,
+ AccountConsistencyModeManager::GetMethodForProfile(incognito_profile));
+ EXPECT_FALSE(AccountConsistencyModeManager::ShouldBuildServiceForProfile(
+ incognito_profile));
+
+ // Non-primary off-the-record profile.
+ Profile* otr_profile = profile.GetOffTheRecordProfile(
+ Profile::OTRProfileID::CreateUniqueForTesting(),
+ /*create_if_needed=*/true);
+ EXPECT_FALSE(
+ AccountConsistencyModeManager::IsDiceEnabledForProfile(otr_profile));
+ EXPECT_FALSE(AccountConsistencyModeManager::GetForProfile(otr_profile));
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled,
+ AccountConsistencyModeManager::GetMethodForProfile(otr_profile));
+ EXPECT_FALSE(AccountConsistencyModeManager::ShouldBuildServiceForProfile(
+ otr_profile));
+ }
+
+ // Guest profile.
+ {
+ TestingProfile::Builder profile_builder;
+ profile_builder.SetGuestSession();
+ std::unique_ptr<Profile> profile = profile_builder.Build();
+ ASSERT_TRUE(profile->IsGuestSession());
+ EXPECT_FALSE(
+ AccountConsistencyModeManager::IsDiceEnabledForProfile(profile.get()));
+ EXPECT_EQ(
+ signin::AccountConsistencyMethod::kDisabled,
+ AccountConsistencyModeManager::GetMethodForProfile(profile.get()));
+ EXPECT_FALSE(AccountConsistencyModeManager::ShouldBuildServiceForProfile(
+ profile.get()));
+ }
+}
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+#if BUILDFLAG(ENABLE_MIRROR)
+// Mirror is enabled by default on Chrome OS, unless specified otherwise.
+TEST(AccountConsistencyModeManagerTest, MirrorEnabledByDefault) {
+ // Creation of this object sets the current thread's id as UI thread.
+ content::BrowserTaskEnvironment task_environment;
+
+ TestingProfile profile;
+ EXPECT_TRUE(
+ AccountConsistencyModeManager::IsMirrorEnabledForProfile(&profile));
+ EXPECT_FALSE(
+ AccountConsistencyModeManager::IsDiceEnabledForProfile(&profile));
+ EXPECT_EQ(signin::AccountConsistencyMethod::kMirror,
+ AccountConsistencyModeManager::GetMethodForProfile(&profile));
+}
+
+TEST(AccountConsistencyModeManagerTest, MirrorDisabledForGuestSession) {
+ // Creation of this object sets the current thread's id as UI thread.
+ content::BrowserTaskEnvironment task_environment;
+
+ TestingProfile profile;
+ profile.SetGuestSession(true);
+ EXPECT_FALSE(
+ AccountConsistencyModeManager::IsMirrorEnabledForProfile(&profile));
+ EXPECT_FALSE(
+ AccountConsistencyModeManager::IsDiceEnabledForProfile(&profile));
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled,
+ AccountConsistencyModeManager::GetMethodForProfile(&profile));
+}
+
+TEST(AccountConsistencyModeManagerTest, MirrorDisabledForOffTheRecordProfile) {
+ // Creation of this object sets the current thread's id as UI thread.
+ content::BrowserTaskEnvironment task_environment;
+
+ TestingProfile profile;
+ Profile* incognito_profile =
+ profile.GetPrimaryOTRProfile(/*create_if_needed=*/true);
+ EXPECT_FALSE(AccountConsistencyModeManager::IsMirrorEnabledForProfile(
+ incognito_profile));
+ EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile(
+ incognito_profile));
+ EXPECT_EQ(
+ signin::AccountConsistencyMethod::kDisabled,
+ AccountConsistencyModeManager::GetMethodForProfile(incognito_profile));
+
+ Profile* otr_profile = profile.GetOffTheRecordProfile(
+ Profile::OTRProfileID::CreateUniqueForTesting(),
+ /*create_if_needed=*/true);
+ EXPECT_FALSE(
+ AccountConsistencyModeManager::IsMirrorEnabledForProfile(otr_profile));
+ EXPECT_FALSE(
+ AccountConsistencyModeManager::IsDiceEnabledForProfile(otr_profile));
+ EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled,
+ AccountConsistencyModeManager::GetMethodForProfile(otr_profile));
+}
+
+#endif // BUILDFLAG(ENABLE_MIRROR)
diff --git a/chromium/chrome/browser/signin/account_id_from_account_info.cc b/chromium/chrome/browser/signin/account_id_from_account_info.cc
new file mode 100644
index 00000000000..c801c7ac983
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_id_from_account_info.cc
@@ -0,0 +1,24 @@
+// Copyright 2019 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.
+
+#include "chrome/browser/signin/account_id_from_account_info.h"
+#include "build/chromeos_buildflags.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "components/user_manager/known_user.h"
+#endif
+
+AccountId AccountIdFromAccountInfo(const CoreAccountInfo& account_info) {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ return user_manager::known_user::GetAccountId(
+ account_info.email, account_info.gaia, AccountType::GOOGLE);
+#else
+ if (account_info.email.empty() || account_info.gaia.empty())
+ return EmptyAccountId();
+
+ return AccountId::FromUserEmailGaiaId(
+ gaia::CanonicalizeEmail(account_info.email), account_info.gaia);
+#endif
+}
diff --git a/chromium/chrome/browser/signin/account_id_from_account_info.h b/chromium/chrome/browser/signin/account_id_from_account_info.h
new file mode 100644
index 00000000000..cdd6836878d
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_id_from_account_info.h
@@ -0,0 +1,18 @@
+// Copyright 2019 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_SIGNIN_ACCOUNT_ID_FROM_ACCOUNT_INFO_H_
+#define CHROME_BROWSER_SIGNIN_ACCOUNT_ID_FROM_ACCOUNT_INFO_H_
+
+#include "components/account_id/account_id.h"
+#include "components/signin/public/identity_manager/account_info.h"
+
+// Returns AccountID populated from |account_info|.
+// NOTE: This utility is in //chrome rather than being part of
+// //components/signin/public because it is only //chrome that needs to go back
+// and forth between AccountId and AccountInfo, and it is outside the scope of
+// //components/signin/public to have knowledge about AccountId.
+AccountId AccountIdFromAccountInfo(const CoreAccountInfo& account_info);
+
+#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_ID_FROM_ACCOUNT_INFO_H_
diff --git a/chromium/chrome/browser/signin/account_id_from_account_info_unittest.cc b/chromium/chrome/browser/signin/account_id_from_account_info_unittest.cc
new file mode 100644
index 00000000000..356e3385e88
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_id_from_account_info_unittest.cc
@@ -0,0 +1,20 @@
+// Copyright 2019 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.
+
+#include "chrome/browser/signin/account_id_from_account_info.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+class AccountIdFromAccountInfoTest : public testing::Test {};
+
+// Tests that AccountIdFromAccountInfo() passes along a canonicalized email to
+// AccountId.
+TEST(AccountIdFromAccountInfoTest,
+ AccountIdFromAccountInfo_CanonicalizesRawEmail) {
+ AccountInfo info;
+ info.email = "test.email@gmail.com";
+ info.gaia = "test_id";
+
+ EXPECT_EQ("testemail@gmail.com",
+ AccountIdFromAccountInfo(info).GetUserEmail());
+}
diff --git a/chromium/chrome/browser/signin/account_investigator_factory.cc b/chromium/chrome/browser/signin/account_investigator_factory.cc
new file mode 100644
index 00000000000..731c8b45a8b
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_investigator_factory.cc
@@ -0,0 +1,57 @@
+// Copyright 2016 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.
+
+#include "chrome/browser/signin/account_investigator_factory.h"
+
+#include "base/memory/singleton.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/prefs/pref_service_factory.h"
+#include "components/signin/core/browser/account_investigator.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+
+// static
+AccountInvestigatorFactory* AccountInvestigatorFactory::GetInstance() {
+ return base::Singleton<AccountInvestigatorFactory>::get();
+}
+
+// static
+AccountInvestigator* AccountInvestigatorFactory::GetForProfile(
+ Profile* profile) {
+ return static_cast<AccountInvestigator*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+AccountInvestigatorFactory::AccountInvestigatorFactory()
+ : BrowserContextKeyedServiceFactory(
+ "AccountInvestigator",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(IdentityManagerFactory::GetInstance());
+}
+
+AccountInvestigatorFactory::~AccountInvestigatorFactory() {}
+
+KeyedService* AccountInvestigatorFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile(Profile::FromBrowserContext(context));
+ AccountInvestigator* investigator = new AccountInvestigator(
+ profile->GetPrefs(), IdentityManagerFactory::GetForProfile(profile));
+ investigator->Initialize();
+ return investigator;
+}
+
+void AccountInvestigatorFactory::RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) {
+ AccountInvestigator::RegisterPrefs(registry);
+}
+
+bool AccountInvestigatorFactory::ServiceIsCreatedWithBrowserContext() const {
+ return true;
+}
+
+bool AccountInvestigatorFactory::ServiceIsNULLWhileTesting() const {
+ return true;
+}
diff --git a/chromium/chrome/browser/signin/account_investigator_factory.h b/chromium/chrome/browser/signin/account_investigator_factory.h
new file mode 100644
index 00000000000..6f1d56caec5
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_investigator_factory.h
@@ -0,0 +1,44 @@
+// Copyright 2016 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_SIGNIN_ACCOUNT_INVESTIGATOR_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_ACCOUNT_INVESTIGATOR_FACTORY_H_
+
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class Profile;
+class AccountInvestigator;
+
+namespace base {
+template <typename T>
+struct DefaultSingletonTraits;
+} // namespace base
+
+// Factory for BrowserKeyedService AccountInvestigator.
+class AccountInvestigatorFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ static AccountInvestigator* GetForProfile(Profile* profile);
+
+ static AccountInvestigatorFactory* GetInstance();
+
+ AccountInvestigatorFactory(const AccountInvestigatorFactory&) = delete;
+ AccountInvestigatorFactory& operator=(const AccountInvestigatorFactory&) =
+ delete;
+
+ private:
+ friend struct base::DefaultSingletonTraits<AccountInvestigatorFactory>;
+
+ AccountInvestigatorFactory();
+ ~AccountInvestigatorFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* context) const override;
+ void RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) override;
+ bool ServiceIsCreatedWithBrowserContext() const override;
+ bool ServiceIsNULLWhileTesting() const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_INVESTIGATOR_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/account_reconcilor_factory.cc b/chromium/chrome/browser/signin/account_reconcilor_factory.cc
new file mode 100644
index 00000000000..9b47faae104
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_reconcilor_factory.cc
@@ -0,0 +1,212 @@
+// Copyright 2013 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.
+
+#include "chrome/browser/signin/account_reconcilor_factory.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/check.h"
+#include "base/feature_list.h"
+#include "base/notreached.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/signin/core/browser/account_reconcilor.h"
+#include "components/signin/core/browser/account_reconcilor_delegate.h"
+#include "components/signin/core/browser/mirror_account_reconcilor_delegate.h"
+#include "components/signin/public/base/account_consistency_method.h"
+#include "components/signin/public/base/signin_buildflags.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "base/metrics/histogram_macros.h"
+#include "base/time/time.h"
+#include "chrome/browser/ash/account_manager/account_manager_util.h"
+#include "chrome/browser/lifetime/application_lifetime.h"
+#include "chromeos/tpm/install_attributes.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/core/browser/active_directory_account_reconcilor_delegate.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/user_manager/user_manager.h"
+#include "google_apis/gaia/google_service_auth_error.h"
+#endif
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+#include "components/signin/core/browser/dice_account_reconcilor_delegate.h"
+#endif
+
+namespace {
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+class ChromeOSLimitedAccessAccountReconcilorDelegate
+ : public signin::MirrorAccountReconcilorDelegate {
+ public:
+ enum class ReconcilorBehavior {
+ kChild,
+ kEnterprise,
+ };
+
+ ChromeOSLimitedAccessAccountReconcilorDelegate(
+ ReconcilorBehavior reconcilor_behavior,
+ signin::IdentityManager* identity_manager)
+ : signin::MirrorAccountReconcilorDelegate(identity_manager),
+ reconcilor_behavior_(reconcilor_behavior) {}
+
+ ChromeOSLimitedAccessAccountReconcilorDelegate(
+ const ChromeOSLimitedAccessAccountReconcilorDelegate&) = delete;
+ ChromeOSLimitedAccessAccountReconcilorDelegate& operator=(
+ const ChromeOSLimitedAccessAccountReconcilorDelegate&) = delete;
+
+ base::TimeDelta GetReconcileTimeout() const override {
+ switch (reconcilor_behavior_) {
+ case ReconcilorBehavior::kChild:
+ return base::Seconds(10);
+ case ReconcilorBehavior::kEnterprise:
+ // 60 seconds is enough to cover about 99% of all reconcile cases.
+ return base::Seconds(60);
+ default:
+ NOTREACHED();
+ return MirrorAccountReconcilorDelegate::GetReconcileTimeout();
+ }
+ }
+
+ void OnReconcileError(const GoogleServiceAuthError& error) override {
+ // If |error| is |GoogleServiceAuthError::State::NONE| or a transient error.
+ if (!error.IsPersistentError()) {
+ return;
+ }
+
+ if (!GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ GetIdentityManager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSignin))) {
+ return;
+ }
+
+ // Mark the account to require an online sign in.
+ const user_manager::User* primary_user =
+ user_manager::UserManager::Get()->GetPrimaryUser();
+ DCHECK(primary_user);
+ user_manager::UserManager::Get()->SaveForceOnlineSignin(
+ primary_user->GetAccountId(), true /* force_online_signin */);
+
+ if (reconcilor_behavior_ == ReconcilorBehavior::kChild) {
+ UMA_HISTOGRAM_BOOLEAN(
+ "ChildAccountReconcilor.ForcedUserExitOnReconcileError", true);
+ }
+ // Force a logout.
+ chrome::AttemptUserExit();
+ }
+
+ private:
+ const ReconcilorBehavior reconcilor_behavior_;
+};
+#endif // BUILDFLAG(IS_CHROMEOS_ASH)
+
+} // namespace
+
+AccountReconcilorFactory::AccountReconcilorFactory()
+ : BrowserContextKeyedServiceFactory(
+ "AccountReconcilor",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(ChromeSigninClientFactory::GetInstance());
+ DependsOn(IdentityManagerFactory::GetInstance());
+}
+
+AccountReconcilorFactory::~AccountReconcilorFactory() {}
+
+// static
+AccountReconcilor* AccountReconcilorFactory::GetForProfile(Profile* profile) {
+ return static_cast<AccountReconcilor*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+AccountReconcilorFactory* AccountReconcilorFactory::GetInstance() {
+ return base::Singleton<AccountReconcilorFactory>::get();
+}
+
+KeyedService* AccountReconcilorFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ SigninClient* signin_client =
+ ChromeSigninClientFactory::GetForProfile(profile);
+ AccountReconcilor* reconcilor =
+ new AccountReconcilor(identity_manager, signin_client,
+ CreateAccountReconcilorDelegate(profile));
+ reconcilor->Initialize(true /* start_reconcile_if_tokens_available */);
+ return reconcilor;
+}
+
+void AccountReconcilorFactory::RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ registry->RegisterBooleanPref(prefs::kForceLogoutUnauthenticatedUserEnabled,
+ false);
+#endif
+}
+
+// static
+std::unique_ptr<signin::AccountReconcilorDelegate>
+AccountReconcilorFactory::CreateAccountReconcilorDelegate(Profile* profile) {
+ signin::AccountConsistencyMethod account_consistency =
+ AccountConsistencyModeManager::GetMethodForProfile(profile);
+ switch (account_consistency) {
+ case signin::AccountConsistencyMethod::kMirror:
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ // Only for child accounts on Chrome OS, use the specialized Mirror
+ // delegate.
+ if (profile->IsChild()) {
+ return std::make_unique<ChromeOSLimitedAccessAccountReconcilorDelegate>(
+ ChromeOSLimitedAccessAccountReconcilorDelegate::ReconcilorBehavior::
+ kChild,
+ IdentityManagerFactory::GetForProfile(profile));
+ }
+
+ // Only for Active Directory accounts on Chrome OS.
+ // TODO(https://crbug.com/993317): Remove the check for
+ // |IsAccountManagerAvailable| after fixing https://crbug.com/1008349 and
+ // https://crbug.com/993317.
+ if (ash::IsAccountManagerAvailable(profile) &&
+ chromeos::InstallAttributes::Get()->IsActiveDirectoryManaged()) {
+ return std::make_unique<
+ signin::ActiveDirectoryAccountReconcilorDelegate>();
+ }
+
+ if (profile->GetPrefs()->GetBoolean(
+ prefs::kForceLogoutUnauthenticatedUserEnabled)) {
+ return std::make_unique<ChromeOSLimitedAccessAccountReconcilorDelegate>(
+ ChromeOSLimitedAccessAccountReconcilorDelegate::ReconcilorBehavior::
+ kEnterprise,
+ IdentityManagerFactory::GetForProfile(profile));
+ }
+
+ return std::make_unique<signin::MirrorAccountReconcilorDelegate>(
+ IdentityManagerFactory::GetForProfile(profile));
+#else
+ return std::make_unique<signin::MirrorAccountReconcilorDelegate>(
+ IdentityManagerFactory::GetForProfile(profile));
+#endif
+
+ case signin::AccountConsistencyMethod::kDisabled:
+ return std::make_unique<signin::AccountReconcilorDelegate>();
+
+ case signin::AccountConsistencyMethod::kDice:
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ return std::make_unique<signin::DiceAccountReconcilorDelegate>();
+#else
+ NOTREACHED();
+ return nullptr;
+#endif
+ }
+
+ NOTREACHED();
+ return nullptr;
+}
diff --git a/chromium/chrome/browser/signin/account_reconcilor_factory.h b/chromium/chrome/browser/signin/account_reconcilor_factory.h
new file mode 100644
index 00000000000..1358fa13cac
--- /dev/null
+++ b/chromium/chrome/browser/signin/account_reconcilor_factory.h
@@ -0,0 +1,57 @@
+// Copyright 2013 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_SIGNIN_ACCOUNT_RECONCILOR_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_ACCOUNT_RECONCILOR_FACTORY_H_
+
+#include <memory>
+
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+namespace signin {
+class IdentityManager;
+}
+
+namespace signin {
+class AccountReconcilorDelegate;
+}
+
+class AccountReconcilor;
+class Profile;
+class SigninClient;
+
+// Singleton that owns all AccountReconcilors and associates them with
+// Profiles. Listens for the Profile's destruction notification and cleans up.
+class AccountReconcilorFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ // Returns the instance of AccountReconcilor associated with this profile
+ // (creating one if none exists). Returns NULL if this profile cannot have an
+ // AccountReconcilor (for example, if |profile| is incognito).
+ static AccountReconcilor* GetForProfile(Profile* profile);
+
+ // Returns an instance of the factory singleton.
+ static AccountReconcilorFactory* GetInstance();
+
+ // BrowserContextKeyedServiceFactory:
+ void RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) override;
+
+ private:
+ friend struct base::DefaultSingletonTraits<AccountReconcilorFactory>;
+ friend class DummyAccountReconcilorWithDelegate; // For testing.
+
+ AccountReconcilorFactory();
+ ~AccountReconcilorFactory() override;
+
+ // Creates the AccountReconcilorDelegate.
+ static std::unique_ptr<signin::AccountReconcilorDelegate>
+ CreateAccountReconcilorDelegate(Profile* profile);
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_RECONCILOR_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/chrome_device_id_helper.cc b/chromium/chrome/browser/signin/chrome_device_id_helper.cc
new file mode 100644
index 00000000000..7b28246d24a
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_device_id_helper.cc
@@ -0,0 +1,87 @@
+// Copyright 2018 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.
+
+#include "chrome/browser/signin/chrome_device_id_helper.h"
+
+#include <string>
+
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/profiles/profile.h"
+#include "components/signin/public/base/device_id_helper.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "base/command_line.h"
+#include "base/guid.h"
+#include "base/logging.h"
+#include "chrome/browser/ash/profiles/profile_helper.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/base/signin_switches.h"
+#include "components/user_manager/known_user.h"
+#include "components/user_manager/user_manager.h"
+#endif
+
+std::string GetSigninScopedDeviceIdForProfile(Profile* profile) {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ if (base::CommandLine::ForCurrentProcess()->HasSwitch(
+ switches::kDisableSigninScopedDeviceId)) {
+ return std::string();
+ }
+
+ // UserManager may not exist in unit_tests.
+ if (!user_manager::UserManager::IsInitialized())
+ return std::string();
+
+ const user_manager::User* user =
+ chromeos::ProfileHelper::Get()->GetUserByProfile(profile);
+ if (!user)
+ return std::string();
+
+ const std::string signin_scoped_device_id =
+ user_manager::known_user::GetDeviceId(user->GetAccountId());
+ LOG_IF(ERROR, signin_scoped_device_id.empty())
+ << "Device ID is not set for user.";
+ return signin_scoped_device_id;
+#else
+ return signin::GetSigninScopedDeviceId(profile->GetPrefs());
+#endif
+}
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+
+std::string GenerateSigninScopedDeviceId(bool for_ephemeral) {
+ constexpr char kEphemeralUserDeviceIDPrefix[] = "t_";
+ std::string guid = base::GenerateGUID();
+ return for_ephemeral ? kEphemeralUserDeviceIDPrefix + guid : guid;
+}
+
+void MigrateSigninScopedDeviceId(Profile* profile) {
+ // UserManager may not exist in unit_tests.
+ if (!user_manager::UserManager::IsInitialized())
+ return;
+
+ const user_manager::User* user =
+ chromeos::ProfileHelper::Get()->GetUserByProfile(profile);
+ if (!user)
+ return;
+ const AccountId account_id = user->GetAccountId();
+ if (user_manager::known_user::GetDeviceId(account_id).empty()) {
+ const std::string legacy_device_id = profile->GetPrefs()->GetString(
+ prefs::kGoogleServicesSigninScopedDeviceId);
+ if (!legacy_device_id.empty()) {
+ // Need to move device ID from the old location to the new one, if it has
+ // not been done yet.
+ user_manager::known_user::SetDeviceId(account_id, legacy_device_id);
+ } else {
+ user_manager::known_user::SetDeviceId(
+ account_id, GenerateSigninScopedDeviceId(
+ user_manager::UserManager::Get()
+ ->IsUserNonCryptohomeDataEphemeral(account_id)));
+ }
+ }
+ profile->GetPrefs()->SetString(prefs::kGoogleServicesSigninScopedDeviceId,
+ std::string());
+}
+
+#endif
diff --git a/chromium/chrome/browser/signin/chrome_device_id_helper.h b/chromium/chrome/browser/signin/chrome_device_id_helper.h
new file mode 100644
index 00000000000..9e84d416528
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_device_id_helper.h
@@ -0,0 +1,36 @@
+// Copyright 2018 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_SIGNIN_CHROME_DEVICE_ID_HELPER_H_
+#define CHROME_BROWSER_SIGNIN_CHROME_DEVICE_ID_HELPER_H_
+
+#include <string>
+
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+
+class Profile;
+
+// Returns the device ID that is scoped to single signin.
+// All refresh tokens for |profile| are annotated with this device ID when they
+// are requested.
+// On non-ChromeOS platforms, this is equivalent to:
+// signin::GetSigninScopedDeviceId(profile->GetPrefs());
+std::string GetSigninScopedDeviceIdForProfile(Profile* profile);
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+
+// Helper method. The device ID should generally be obtained through
+// GetSigninScopedDeviceIdForProfile().
+// If |for_ephemeral| is true, special kind of device ID for ephemeral users is
+// generated.
+std::string GenerateSigninScopedDeviceId(bool for_ephemeral);
+
+// Moves any existing device ID out of the pref service into the UserManager,
+// and creates a new ID if it is empty.
+void MigrateSigninScopedDeviceId(Profile* profile);
+
+#endif
+
+#endif // CHROME_BROWSER_SIGNIN_CHROME_DEVICE_ID_HELPER_H_
diff --git a/chromium/chrome/browser/signin/chrome_device_id_helper_unittest.cc b/chromium/chrome/browser/signin/chrome_device_id_helper_unittest.cc
new file mode 100644
index 00000000000..3e2f2e9517b
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_device_id_helper_unittest.cc
@@ -0,0 +1,40 @@
+// Copyright 2018 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.
+
+#include "chrome/browser/signin/chrome_device_id_helper.h"
+
+#include <string>
+
+#include "base/strings/string_util.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+const char kEpehemeralPrefix[] = "t_";
+
+TEST(DeviceIdHelper, NotEphemeral) {
+ std::string device_id =
+ GenerateSigninScopedDeviceId(false /* for_ephemeral */);
+ // Not empty.
+ EXPECT_FALSE(device_id.empty());
+ // No ephemeral prefix.
+ EXPECT_FALSE(base::StartsWith(device_id, kEpehemeralPrefix,
+ base::CompareCase::SENSITIVE));
+ // ID is unique.
+ EXPECT_NE(device_id, GenerateSigninScopedDeviceId(false /* for_ephemeral */));
+}
+
+TEST(DeviceIdHelper, Ephemeral) {
+ std::string device_id =
+ GenerateSigninScopedDeviceId(true /* for_ephemeral */);
+ // Ephemeral prefix.
+ EXPECT_TRUE(base::StartsWith(device_id, kEpehemeralPrefix,
+ base::CompareCase::SENSITIVE));
+ // Not empty.
+ EXPECT_NE(device_id, kEpehemeralPrefix);
+ // ID is unique.
+ EXPECT_NE(device_id, GenerateSigninScopedDeviceId(true /* for_ephemeral */));
+}
+#endif
diff --git a/chromium/chrome/browser/signin/chrome_signin_client.cc b/chromium/chrome/browser/signin/chrome_signin_client.cc
new file mode 100644
index 00000000000..3a5ea106adb
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_client.cc
@@ -0,0 +1,369 @@
+// Copyright 2014 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.
+
+#include "chrome/browser/signin/chrome_signin_client.h"
+
+#include <stddef.h>
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/strings/utf_string_conversions.h"
+#include "build/build_config.h"
+#include "build/buildflag.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/content_settings/cookie_settings_factory.h"
+#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
+#include "chrome/browser/enterprise/util/managed_browser_utils.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/profiles/profile_metrics.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/chrome_device_id_helper.h"
+#include "chrome/browser/signin/force_signin_verifier.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "chrome/browser/signin/signin_util.h"
+#include "chrome/common/buildflags.h"
+#include "chrome/common/channel_info.h"
+#include "chrome/common/pref_names.h"
+#include "components/content_settings/core/browser/cookie_settings.h"
+#include "components/metrics/metrics_service.h"
+#include "components/policy/core/browser/browser_policy_connector.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/core/browser/cookie_settings_util.h"
+#include "components/signin/public/base/signin_buildflags.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/access_token_info.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/scope_set.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/network_service_instance.h"
+#include "content/public/browser/storage_partition.h"
+#include "google_apis/gaia/gaia_constants.h"
+#include "google_apis/gaia/gaia_urls.h"
+#include "url/gurl.h"
+
+#if BUILDFLAG(ENABLE_SUPERVISED_USERS)
+#include "chrome/browser/supervised_user/supervised_user_constants.h"
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/ash/net/delay_network_call.h"
+#include "chromeos/network/network_handler.h"
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+#include "chrome/browser/lacros/account_manager/account_manager_util.h"
+#include "chromeos/crosapi/mojom/account_manager.mojom.h"
+#include "chromeos/lacros/lacros_service.h"
+#include "components/account_manager_core/account.h"
+#include "components/account_manager_core/account_manager_util.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#endif
+
+#if !defined(OS_ANDROID)
+#include "chrome/browser/profiles/profile_window.h"
+#endif
+
+#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/browser/ui/profile_picker.h"
+#endif
+
+namespace {
+
+// List of sources for which sign out is always allowed.
+signin_metrics::ProfileSignout kAlwaysAllowedSignoutSources[] = {
+ // Allowed, because data has not been synced yet.
+ signin_metrics::ProfileSignout::ABORT_SIGNIN,
+ // Allowed, because only used on Android and the primary account must be
+ // cleared when the account is removed from device
+ signin_metrics::ProfileSignout::ACCOUNT_REMOVED_FROM_DEVICE,
+ // Allowed to force finish the account id migration.
+ signin_metrics::ACCOUNT_ID_MIGRATION,
+ // Allowed, for tests.
+ signin_metrics::ProfileSignout::FORCE_SIGNOUT_ALWAYS_ALLOWED_FOR_TEST};
+
+SigninClient::SignoutDecision IsSignoutAllowed(
+ Profile* profile,
+ const signin_metrics::ProfileSignout signout_source) {
+ if (signin_util::IsUserSignoutAllowedForProfile(profile))
+ return SigninClient::SignoutDecision::ALLOW_SIGNOUT;
+
+ auto* identity_manager =
+ IdentityManagerFactory::GetForProfileIfExists(profile);
+ if (identity_manager &&
+ !identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) {
+ return SigninClient::SignoutDecision::ALLOW_SIGNOUT;
+ }
+
+ for (const auto& always_allowed_source : kAlwaysAllowedSignoutSources) {
+ if (signout_source == always_allowed_source)
+ return SigninClient::SignoutDecision::ALLOW_SIGNOUT;
+ }
+
+ return SigninClient::SignoutDecision::DISALLOW_SIGNOUT;
+}
+
+} // namespace
+
+ChromeSigninClient::ChromeSigninClient(Profile* profile) : profile_(profile) {
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+ content::GetNetworkConnectionTracker()->AddNetworkConnectionObserver(this);
+#endif
+}
+
+ChromeSigninClient::~ChromeSigninClient() {
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+ content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver(this);
+#endif
+}
+
+void ChromeSigninClient::DoFinalInit() {
+ VerifySyncToken();
+}
+
+// static
+bool ChromeSigninClient::ProfileAllowsSigninCookies(Profile* profile) {
+ content_settings::CookieSettings* cookie_settings =
+ CookieSettingsFactory::GetForProfile(profile).get();
+ return signin::SettingsAllowSigninCookies(cookie_settings);
+}
+
+PrefService* ChromeSigninClient::GetPrefs() { return profile_->GetPrefs(); }
+
+scoped_refptr<network::SharedURLLoaderFactory>
+ChromeSigninClient::GetURLLoaderFactory() {
+ if (url_loader_factory_for_testing_)
+ return url_loader_factory_for_testing_;
+
+ return profile_->GetDefaultStoragePartition()
+ ->GetURLLoaderFactoryForBrowserProcess();
+}
+
+network::mojom::CookieManager* ChromeSigninClient::GetCookieManager() {
+ return profile_->GetDefaultStoragePartition()
+ ->GetCookieManagerForBrowserProcess();
+}
+
+bool ChromeSigninClient::AreSigninCookiesAllowed() {
+ return ProfileAllowsSigninCookies(profile_);
+}
+
+bool ChromeSigninClient::AreSigninCookiesDeletedOnExit() {
+ content_settings::CookieSettings* cookie_settings =
+ CookieSettingsFactory::GetForProfile(profile_).get();
+ return signin::SettingsDeleteSigninCookiesOnExit(cookie_settings);
+}
+
+void ChromeSigninClient::AddContentSettingsObserver(
+ content_settings::Observer* observer) {
+ HostContentSettingsMapFactory::GetForProfile(profile_)
+ ->AddObserver(observer);
+}
+
+void ChromeSigninClient::RemoveContentSettingsObserver(
+ content_settings::Observer* observer) {
+ HostContentSettingsMapFactory::GetForProfile(profile_)
+ ->RemoveObserver(observer);
+}
+
+void ChromeSigninClient::PreSignOut(
+ base::OnceCallback<void(SignoutDecision)> on_signout_decision_reached,
+ signin_metrics::ProfileSignout signout_source_metric) {
+ DCHECK(on_signout_decision_reached);
+ DCHECK(!on_signout_decision_reached_) << "SignOut already in-progress!";
+ on_signout_decision_reached_ = std::move(on_signout_decision_reached);
+
+#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH)
+ // `signout_source_metric` is `signin_metrics::ABORT_SIGNIN` if the user
+ // declines sync in the signin process. In case the user accepts the managed
+ // account but declines sync, we should keep the window open.
+ bool user_declines_sync_after_consenting_to_management =
+ signout_source_metric == signin_metrics::ABORT_SIGNIN &&
+ chrome::enterprise_util::UserAcceptedAccountManagement(profile_);
+ // These sign out won't remove the policy cache, keep the window opened.
+ bool keep_window_opened =
+ signout_source_metric ==
+ signin_metrics::GOOGLE_SERVICE_NAME_PATTERN_CHANGED ||
+ signout_source_metric == signin_metrics::SERVER_FORCED_DISABLE ||
+ signout_source_metric == signin_metrics::SIGNOUT_PREF_CHANGED ||
+ user_declines_sync_after_consenting_to_management;
+ if (signin_util::IsForceSigninEnabled() && !profile_->IsSystemProfile() &&
+ !profile_->IsGuestSession() && !profile_->IsChild() &&
+ !keep_window_opened) {
+ if (signout_source_metric ==
+ signin_metrics::SIGNIN_PREF_CHANGED_DURING_SIGNIN) {
+ // SIGNIN_PREF_CHANGED_DURING_SIGNIN will be triggered when
+ // IdentityManager is initialized before window opening, there is no need
+ // to close window. Call OnCloseBrowsersSuccess to continue sign out and
+ // show UserManager afterwards.
+ should_display_user_manager_ = false; // Don't show UserManager twice.
+ OnCloseBrowsersSuccess(signout_source_metric, profile_->GetPath());
+ } else {
+ BrowserList::CloseAllBrowsersWithProfile(
+ profile_,
+ base::BindRepeating(&ChromeSigninClient::OnCloseBrowsersSuccess,
+ base::Unretained(this), signout_source_metric),
+ base::BindRepeating(&ChromeSigninClient::OnCloseBrowsersAborted,
+ base::Unretained(this)),
+ signout_source_metric == signin_metrics::ABORT_SIGNIN ||
+ signout_source_metric ==
+ signin_metrics::AUTHENTICATION_FAILED_WITH_FORCE_SIGNIN ||
+ signout_source_metric == signin_metrics::TRANSFER_CREDENTIALS);
+ }
+ } else {
+#else
+ {
+#endif
+ std::move(on_signout_decision_reached_)
+ .Run(IsSignoutAllowed(profile_, signout_source_metric));
+ }
+}
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+void ChromeSigninClient::OnConnectionChanged(
+ network::mojom::ConnectionType type) {
+ if (type == network::mojom::ConnectionType::CONNECTION_NONE)
+ return;
+
+ for (base::OnceClosure& callback : delayed_callbacks_)
+ std::move(callback).Run();
+
+ delayed_callbacks_.clear();
+}
+#endif
+
+void ChromeSigninClient::DelayNetworkCall(base::OnceClosure callback) {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ // Do not make network requests in unit tests. chromeos::NetworkHandler should
+ // not be used and is not expected to have been initialized in unit tests.
+ if (url_loader_factory_for_testing_ &&
+ !chromeos::NetworkHandler::IsInitialized()) {
+ std::move(callback).Run();
+ return;
+ }
+ chromeos::DelayNetworkCall(
+ base::Milliseconds(chromeos::kDefaultNetworkRetryDelayMS),
+ std::move(callback));
+ return;
+#else
+ // Don't bother if we don't have any kind of network connection.
+ network::mojom::ConnectionType type;
+ bool sync = content::GetNetworkConnectionTracker()->GetConnectionType(
+ &type, base::BindOnce(&ChromeSigninClient::OnConnectionChanged,
+ weak_ptr_factory_.GetWeakPtr()));
+ if (!sync || type == network::mojom::ConnectionType::CONNECTION_NONE) {
+ // Connection type cannot be retrieved synchronously so delay the callback.
+ delayed_callbacks_.push_back(std::move(callback));
+ } else {
+ std::move(callback).Run();
+ }
+#endif
+}
+
+std::unique_ptr<GaiaAuthFetcher> ChromeSigninClient::CreateGaiaAuthFetcher(
+ GaiaAuthConsumer* consumer,
+ gaia::GaiaSource source) {
+ return std::make_unique<GaiaAuthFetcher>(consumer, source,
+ GetURLLoaderFactory());
+}
+
+void ChromeSigninClient::VerifySyncToken() {
+#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH)
+ // We only verifiy the token once when Profile is just created.
+ if (signin_util::IsForceSigninEnabled() && !force_signin_verifier_)
+ force_signin_verifier_ = std::make_unique<ForceSigninVerifier>(
+ profile_, IdentityManagerFactory::GetForProfile(profile_));
+#endif
+}
+
+bool ChromeSigninClient::IsNonEnterpriseUser(const std::string& username) {
+ return policy::BrowserPolicyConnector::IsNonEnterpriseUser(username);
+}
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+// Returns the account that must be auto-signed-in to the Main Profile in
+// Lacros.
+// This is, when available, the account used to sign into the Chrome OS
+// session. This may be a Gaia account or a Microsoft Active Directory
+// account. This field will be null for Guest sessions, Managed Guest
+// sessions, Demo mode, and Kiosks. Note that this is different from the
+// concept of a Primary Account in the browser. A user may not be signed into
+// a Lacros browser Profile, or may be signed into a browser Profile with an
+// account which is different from the account which they used to sign into
+// the device - aka Device Account.
+// Also note that this will be null for Secondary / non-Main Profiles in
+// Lacros, because they do not start with the Chrome OS Device Account
+// signed-in by default.
+absl::optional<account_manager::Account>
+ChromeSigninClient::GetInitialPrimaryAccount() {
+ if (!profile_->IsMainProfile())
+ return absl::nullopt;
+
+ const crosapi::mojom::AccountPtr& device_account =
+ chromeos::LacrosService::Get()->init_params()->device_account;
+ if (!device_account)
+ return absl::nullopt;
+
+ return account_manager::FromMojoAccount(device_account);
+}
+#endif
+
+void ChromeSigninClient::SetURLLoaderFactoryForTest(
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) {
+ url_loader_factory_for_testing_ = url_loader_factory;
+}
+
+void ChromeSigninClient::OnCloseBrowsersSuccess(
+ const signin_metrics::ProfileSignout signout_source_metric,
+ const base::FilePath& profile_path) {
+#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH)
+ if (signin_util::IsForceSigninEnabled() && force_signin_verifier_.get()) {
+ force_signin_verifier_->Cancel();
+ }
+#endif
+
+ std::move(on_signout_decision_reached_)
+ .Run(IsSignoutAllowed(profile_, signout_source_metric));
+
+ LockForceSigninProfile(profile_path);
+ // After sign out, lock the profile and show UserManager if necessary.
+ if (should_display_user_manager_) {
+ ShowUserManager(profile_path);
+ } else {
+ should_display_user_manager_ = true;
+ }
+}
+
+void ChromeSigninClient::OnCloseBrowsersAborted(
+ const base::FilePath& profile_path) {
+ should_display_user_manager_ = true;
+
+ // Disallow sign-out (aborted).
+ std::move(on_signout_decision_reached_)
+ .Run(SignoutDecision::DISALLOW_SIGNOUT);
+}
+
+void ChromeSigninClient::LockForceSigninProfile(
+ const base::FilePath& profile_path) {
+ ProfileAttributesEntry* entry =
+ g_browser_process->profile_manager()
+ ->GetProfileAttributesStorage()
+ .GetProfileAttributesWithPath(profile_->GetPath());
+ if (!entry)
+ return;
+ entry->LockForceSigninProfile(true);
+}
+
+void ChromeSigninClient::ShowUserManager(const base::FilePath& profile_path) {
+#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH)
+ ProfilePicker::Show(ProfilePicker::EntryPoint::kProfileLocked);
+#endif
+}
diff --git a/chromium/chrome/browser/signin/chrome_signin_client.h b/chromium/chrome/browser/signin/chrome_signin_client.h
new file mode 100644
index 00000000000..cef8fd3c32e
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_client.h
@@ -0,0 +1,115 @@
+// Copyright 2014 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_SIGNIN_CHROME_SIGNIN_CLIENT_H_
+#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_H_
+
+#include <list>
+#include <memory>
+#include <string>
+
+#include "base/memory/raw_ptr.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/memory/weak_ptr.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "components/signin/public/base/signin_client.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+#include "services/network/public/mojom/network_change_manager.mojom-forward.h"
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+#include "services/network/public/cpp/network_connection_tracker.h"
+#endif
+
+#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH)
+class ForceSigninVerifier;
+#endif
+class Profile;
+
+class ChromeSigninClient
+ : public SigninClient
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+ ,
+ public network::NetworkConnectionTracker::NetworkConnectionObserver
+#endif
+{
+ public:
+ explicit ChromeSigninClient(Profile* profile);
+
+ ChromeSigninClient(const ChromeSigninClient&) = delete;
+ ChromeSigninClient& operator=(const ChromeSigninClient&) = delete;
+
+ ~ChromeSigninClient() override;
+
+ void DoFinalInit() override;
+
+ // Utility method.
+ static bool ProfileAllowsSigninCookies(Profile* profile);
+
+ // SigninClient implementation.
+ PrefService* GetPrefs() override;
+ void PreSignOut(
+ base::OnceCallback<void(SignoutDecision)> on_signout_decision_reached,
+ signin_metrics::ProfileSignout signout_source_metric) override;
+ scoped_refptr<network::SharedURLLoaderFactory> GetURLLoaderFactory() override;
+ network::mojom::CookieManager* GetCookieManager() override;
+ bool AreSigninCookiesAllowed() override;
+ bool AreSigninCookiesDeletedOnExit() override;
+ void AddContentSettingsObserver(
+ content_settings::Observer* observer) override;
+ void RemoveContentSettingsObserver(
+ content_settings::Observer* observer) override;
+ void DelayNetworkCall(base::OnceClosure callback) override;
+ std::unique_ptr<GaiaAuthFetcher> CreateGaiaAuthFetcher(
+ GaiaAuthConsumer* consumer,
+ gaia::GaiaSource source) override;
+ bool IsNonEnterpriseUser(const std::string& username) override;
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+ // network::NetworkConnectionTracker::NetworkConnectionObserver
+ // implementation.
+ void OnConnectionChanged(network::mojom::ConnectionType type) override;
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+ absl::optional<account_manager::Account> GetInitialPrimaryAccount() override;
+#endif
+
+ // Used in tests to override the URLLoaderFactory returned by
+ // GetURLLoaderFactory().
+ void SetURLLoaderFactoryForTest(
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory);
+
+ protected:
+ virtual void ShowUserManager(const base::FilePath& profile_path);
+ virtual void LockForceSigninProfile(const base::FilePath& profile_path);
+
+ private:
+ void VerifySyncToken();
+ void OnCloseBrowsersSuccess(
+ const signin_metrics::ProfileSignout signout_source_metric,
+ const base::FilePath& profile_path);
+ void OnCloseBrowsersAborted(const base::FilePath& profile_path);
+
+ raw_ptr<Profile> profile_;
+
+ // Stored callback from PreSignOut();
+ base::OnceCallback<void(SignoutDecision)> on_signout_decision_reached_;
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+ std::list<base::OnceClosure> delayed_callbacks_;
+#endif
+
+ bool should_display_user_manager_ = true;
+#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH)
+ std::unique_ptr<ForceSigninVerifier> force_signin_verifier_;
+#endif
+
+ scoped_refptr<network::SharedURLLoaderFactory>
+ url_loader_factory_for_testing_;
+
+ base::WeakPtrFactory<ChromeSigninClient> weak_ptr_factory_{this};
+};
+
+#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_H_
diff --git a/chromium/chrome/browser/signin/chrome_signin_client_factory.cc b/chromium/chrome/browser/signin/chrome_signin_client_factory.cc
new file mode 100644
index 00000000000..62c0d638cbd
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_client_factory.cc
@@ -0,0 +1,34 @@
+// Copyright 2014 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.
+
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+
+#include "chrome/browser/net/profile_network_context_service_factory.h"
+#include "chrome/browser/profiles/profile.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+
+ChromeSigninClientFactory::ChromeSigninClientFactory()
+ : BrowserContextKeyedServiceFactory(
+ "ChromeSigninClient",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(ProfileNetworkContextServiceFactory::GetInstance());
+}
+
+ChromeSigninClientFactory::~ChromeSigninClientFactory() {}
+
+// static
+SigninClient* ChromeSigninClientFactory::GetForProfile(Profile* profile) {
+ return static_cast<SigninClient*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+ChromeSigninClientFactory* ChromeSigninClientFactory::GetInstance() {
+ return base::Singleton<ChromeSigninClientFactory>::get();
+}
+
+KeyedService* ChromeSigninClientFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ return new ChromeSigninClient(Profile::FromBrowserContext(context));
+}
diff --git a/chromium/chrome/browser/signin/chrome_signin_client_factory.h b/chromium/chrome/browser/signin/chrome_signin_client_factory.h
new file mode 100644
index 00000000000..e88d3f23b00
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_client_factory.h
@@ -0,0 +1,37 @@
+// Copyright 2014 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_SIGNIN_CHROME_SIGNIN_CLIENT_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "chrome/browser/signin/chrome_signin_client.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class Profile;
+
+// Singleton that owns all ChromeSigninClients and associates them with
+// Profiles.
+class ChromeSigninClientFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ // Returns the instance of SigninClient associated with this profile
+ // (creating one if none exists). Returns NULL if this profile cannot have an
+ // SigninClient (for example, if |profile| is incognito).
+ static SigninClient* GetForProfile(Profile* profile);
+
+ // Returns an instance of the factory singleton.
+ static ChromeSigninClientFactory* GetInstance();
+
+ private:
+ friend struct base::DefaultSingletonTraits<ChromeSigninClientFactory>;
+
+ ChromeSigninClientFactory();
+ ~ChromeSigninClientFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/chrome_signin_client_test_util.cc b/chromium/chrome/browser/signin/chrome_signin_client_test_util.cc
new file mode 100644
index 00000000000..e0412419de1
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_client_test_util.cc
@@ -0,0 +1,21 @@
+// Copyright 2016 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.
+
+#include "chrome/browser/signin/chrome_signin_client_test_util.h"
+
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/chrome_signin_client.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
+#include "services/network/test/test_url_loader_factory.h"
+
+std::unique_ptr<KeyedService> BuildChromeSigninClientWithURLLoader(
+ network::TestURLLoaderFactory* test_url_loader_factory,
+ content::BrowserContext* context) {
+ Profile* profile = Profile::FromBrowserContext(context);
+ auto signin_client = std::make_unique<ChromeSigninClient>(profile);
+ signin_client->SetURLLoaderFactoryForTest(
+ test_url_loader_factory->GetSafeWeakWrapper());
+ return signin_client;
+}
diff --git a/chromium/chrome/browser/signin/chrome_signin_client_test_util.h b/chromium/chrome/browser/signin/chrome_signin_client_test_util.h
new file mode 100644
index 00000000000..5c76977980f
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_client_test_util.h
@@ -0,0 +1,26 @@
+// Copyright 2016 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_SIGNIN_CHROME_SIGNIN_CLIENT_TEST_UTIL_H_
+#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_TEST_UTIL_H_
+
+#include <memory>
+
+class KeyedService;
+
+namespace content {
+class BrowserContext;
+}
+
+namespace network {
+class TestURLLoaderFactory;
+}
+
+// Creates a ChromeSigninClient using the supplied
+// |test_url_loader_factory| and |context|.
+std::unique_ptr<KeyedService> BuildChromeSigninClientWithURLLoader(
+ network::TestURLLoaderFactory* test_url_loader_factory,
+ content::BrowserContext* context);
+
+#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_TEST_UTIL_H_
diff --git a/chromium/chrome/browser/signin/chrome_signin_client_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_client_unittest.cc
new file mode 100644
index 00000000000..ed2ac0db988
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_client_unittest.cc
@@ -0,0 +1,438 @@
+// Copyright 2015 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.
+
+#include "chrome/browser/signin/chrome_signin_client.h"
+#include <memory>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/cxx17_backports.h"
+#include "base/feature_list.h"
+#include "base/memory/raw_ptr.h"
+#include "base/notreached.h"
+#include "base/run_loop.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/enterprise/util/managed_browser_utils.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "chrome/browser/signin/signin_util.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "chrome/test/base/testing_profile.h"
+#include "chrome/test/base/testing_profile_manager.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/consent_level.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "content/public/browser/network_service_instance.h"
+#include "content/public/test/browser_task_environment.h"
+#include "services/network/test/test_network_connection_tracker.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+#if !defined(OS_ANDROID)
+#include "chrome/test/base/browser_with_test_window_test.h"
+#endif
+
+// ChromeOS has its own network delay logic.
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+
+namespace {
+
+class CallbackTester {
+ public:
+ CallbackTester() : called_(0) {}
+
+ void Increment();
+ void IncrementAndUnblock(base::RunLoop* run_loop);
+ bool WasCalledExactlyOnce();
+
+ private:
+ int called_;
+};
+
+void CallbackTester::Increment() {
+ called_++;
+}
+
+void CallbackTester::IncrementAndUnblock(base::RunLoop* run_loop) {
+ Increment();
+ run_loop->QuitWhenIdle();
+}
+
+bool CallbackTester::WasCalledExactlyOnce() {
+ return called_ == 1;
+}
+
+} // namespace
+
+class ChromeSigninClientTest : public testing::Test {
+ public:
+ ChromeSigninClientTest() {
+ // Create a signed-in profile.
+ TestingProfile::Builder builder;
+ profile_ = builder.Build();
+
+ signin_client_ = ChromeSigninClientFactory::GetForProfile(profile());
+ }
+
+ protected:
+ void SetUpNetworkConnection(bool respond_synchronously,
+ network::mojom::ConnectionType connection_type) {
+ auto* tracker = network::TestNetworkConnectionTracker::GetInstance();
+ tracker->SetRespondSynchronously(respond_synchronously);
+ tracker->SetConnectionType(connection_type);
+ }
+
+ void SetConnectionType(network::mojom::ConnectionType connection_type) {
+ network::TestNetworkConnectionTracker::GetInstance()->SetConnectionType(
+ connection_type);
+ }
+
+ Profile* profile() { return profile_.get(); }
+ SigninClient* signin_client() { return signin_client_; }
+
+ private:
+ content::BrowserTaskEnvironment task_environment_;
+ std::unique_ptr<Profile> profile_;
+ raw_ptr<SigninClient> signin_client_;
+};
+
+TEST_F(ChromeSigninClientTest, DelayNetworkCallRunsImmediatelyWithNetwork) {
+ SetUpNetworkConnection(true, network::mojom::ConnectionType::CONNECTION_3G);
+ CallbackTester tester;
+ signin_client()->DelayNetworkCall(
+ base::BindOnce(&CallbackTester::Increment, base::Unretained(&tester)));
+ ASSERT_TRUE(tester.WasCalledExactlyOnce());
+}
+
+TEST_F(ChromeSigninClientTest, DelayNetworkCallRunsAfterGetConnectionType) {
+ SetUpNetworkConnection(false, network::mojom::ConnectionType::CONNECTION_3G);
+
+ base::RunLoop run_loop;
+ CallbackTester tester;
+ signin_client()->DelayNetworkCall(
+ base::BindOnce(&CallbackTester::IncrementAndUnblock,
+ base::Unretained(&tester), &run_loop));
+ ASSERT_FALSE(tester.WasCalledExactlyOnce());
+ run_loop.Run(); // Wait for IncrementAndUnblock().
+ ASSERT_TRUE(tester.WasCalledExactlyOnce());
+}
+
+TEST_F(ChromeSigninClientTest, DelayNetworkCallRunsAfterNetworkChange) {
+ SetUpNetworkConnection(true, network::mojom::ConnectionType::CONNECTION_NONE);
+
+ base::RunLoop run_loop;
+ CallbackTester tester;
+ signin_client()->DelayNetworkCall(
+ base::BindOnce(&CallbackTester::IncrementAndUnblock,
+ base::Unretained(&tester), &run_loop));
+
+ ASSERT_FALSE(tester.WasCalledExactlyOnce());
+ SetConnectionType(network::mojom::ConnectionType::CONNECTION_3G);
+ run_loop.Run(); // Wait for IncrementAndUnblock().
+ ASSERT_TRUE(tester.WasCalledExactlyOnce());
+}
+
+#if !defined(OS_ANDROID)
+
+class MockChromeSigninClient : public ChromeSigninClient {
+ public:
+ explicit MockChromeSigninClient(Profile* profile)
+ : ChromeSigninClient(profile) {}
+
+ MOCK_METHOD1(ShowUserManager, void(const base::FilePath&));
+ MOCK_METHOD1(LockForceSigninProfile, void(const base::FilePath&));
+
+ MOCK_METHOD3(SignOutCallback,
+ void(signin_metrics::ProfileSignout,
+ signin_metrics::SignoutDelete,
+ SigninClient::SignoutDecision signout_decision));
+};
+
+class ChromeSigninClientSignoutTest : public BrowserWithTestWindowTest {
+ public:
+ ChromeSigninClientSignoutTest() : forced_signin_setter_(true) {}
+ void SetUp() override {
+ BrowserWithTestWindowTest::SetUp();
+ CreateClient(browser()->profile());
+ }
+
+ void TearDown() override {
+ BrowserWithTestWindowTest::TearDown();
+ TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
+ }
+
+ void CreateClient(Profile* profile) {
+ client_ = std::make_unique<MockChromeSigninClient>(profile);
+ }
+
+ void PreSignOut(signin_metrics::ProfileSignout source_metric,
+ signin_metrics::SignoutDelete delete_metric) {
+ client_->PreSignOut(base::BindOnce(&MockChromeSigninClient::SignOutCallback,
+ base::Unretained(client_.get()),
+ source_metric, delete_metric),
+ source_metric);
+ }
+
+ signin_util::ScopedForceSigninSetterForTesting forced_signin_setter_;
+ std::unique_ptr<MockChromeSigninClient> client_;
+};
+
+TEST_F(ChromeSigninClientSignoutTest, SignOut) {
+ signin_metrics::ProfileSignout source_metric =
+ signin_metrics::ProfileSignout::USER_CLICKED_SIGNOUT_SETTINGS;
+ signin_metrics::SignoutDelete delete_metric =
+ signin_metrics::SignoutDelete::kIgnoreMetric;
+
+ EXPECT_CALL(*client_, ShowUserManager(browser()->profile()->GetPath()))
+ .Times(1);
+ EXPECT_CALL(*client_, LockForceSigninProfile(browser()->profile()->GetPath()))
+ .Times(1);
+ EXPECT_CALL(
+ *client_,
+ SignOutCallback(source_metric, delete_metric,
+ SigninClient::SignoutDecision::ALLOW_SIGNOUT))
+ .Times(1);
+
+ PreSignOut(source_metric, delete_metric);
+}
+
+TEST_F(ChromeSigninClientSignoutTest, SignOutWithoutForceSignin) {
+ signin_util::ScopedForceSigninSetterForTesting signin_setter(false);
+ CreateClient(browser()->profile());
+
+ signin_metrics::ProfileSignout source_metric =
+ signin_metrics::ProfileSignout::USER_CLICKED_SIGNOUT_SETTINGS;
+ signin_metrics::SignoutDelete delete_metric =
+ signin_metrics::SignoutDelete::kIgnoreMetric;
+
+ EXPECT_CALL(*client_, ShowUserManager(browser()->profile()->GetPath()))
+ .Times(0);
+ EXPECT_CALL(*client_, LockForceSigninProfile(browser()->profile()->GetPath()))
+ .Times(0);
+ EXPECT_CALL(
+ *client_,
+ SignOutCallback(source_metric, delete_metric,
+ SigninClient::SignoutDecision::ALLOW_SIGNOUT))
+ .Times(1);
+ PreSignOut(source_metric, delete_metric);
+}
+
+class ChromeSigninClientSignoutSourceTest
+ : public ::testing::WithParamInterface<signin_metrics::ProfileSignout>,
+ public ChromeSigninClientSignoutTest {
+ protected:
+ signin::IdentityTestEnvironment* identity_test_env() {
+ return &identity_test_env_;
+ }
+
+ private:
+ signin::IdentityTestEnvironment identity_test_env_;
+};
+
+// Returns true if signout can be disallowed by policy for the given source.
+bool IsSignoutDisallowedByPolicy(
+ Profile* profile,
+ signin_metrics::ProfileSignout signout_source) {
+ auto* identity_manager =
+ IdentityManagerFactory::GetForProfileIfExists(profile);
+ if (identity_manager &&
+ !identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) {
+ return false;
+ }
+
+ switch (signout_source) {
+ // NOTE: SIGNOUT_TEST == SIGNOUT_PREF_CHANGED.
+ case signin_metrics::ProfileSignout::SIGNOUT_PREF_CHANGED:
+ case signin_metrics::ProfileSignout::GOOGLE_SERVICE_NAME_PATTERN_CHANGED:
+ case signin_metrics::ProfileSignout::SIGNIN_PREF_CHANGED_DURING_SIGNIN:
+ case signin_metrics::ProfileSignout::USER_CLICKED_SIGNOUT_SETTINGS:
+ case signin_metrics::ProfileSignout::SERVER_FORCED_DISABLE:
+ case signin_metrics::ProfileSignout::TRANSFER_CREDENTIALS:
+ case signin_metrics::ProfileSignout::
+ AUTHENTICATION_FAILED_WITH_FORCE_SIGNIN:
+ case signin_metrics::ProfileSignout::SIGNIN_NOT_ALLOWED_ON_PROFILE_INIT:
+ case signin_metrics::ProfileSignout::USER_TUNED_OFF_SYNC_FROM_DICE_UI:
+ return true;
+ case signin_metrics::ProfileSignout::ACCOUNT_REMOVED_FROM_DEVICE:
+ case signin_metrics::ProfileSignout::
+ IOS_ACCOUNT_REMOVED_FROM_DEVICE_AFTER_RESTORE:
+ // TODO(msarda): Add more of the above cases to this "false" branch.
+ // For now only ACCOUNT_REMOVED_FROM_DEVICE is here to preserve the status
+ // quo. Additional internal sources of sign-out will be moved here in a
+ // follow up CL.
+ return false;
+ case signin_metrics::ProfileSignout::ABORT_SIGNIN:
+ // Allow signout because data has not been synced yet.
+ return false;
+ case signin_metrics::ProfileSignout::FORCE_SIGNOUT_ALWAYS_ALLOWED_FOR_TEST:
+ // Allow signout for tests that want to force it.
+ return false;
+ case signin_metrics::ProfileSignout::ACCOUNT_ID_MIGRATION:
+ // Allowed to force finish the account id migration.
+ return false;
+ case signin_metrics::ProfileSignout::USER_DELETED_ACCOUNT_COOKIES:
+ case signin_metrics::ProfileSignout::MOBILE_IDENTITY_CONSISTENCY_ROLLBACK:
+ // There's no special-casing for these in ChromeSigninClient, as they only
+ // happen when there's no sync account and policies aren't enforced.
+ // PrimaryAccountManager won't actually invoke PreSignOut in this case,
+ // thus it is fine for ChromeSigninClient to not have any special-casing.
+ return true;
+ case signin_metrics::ProfileSignout::NUM_PROFILE_SIGNOUT_METRICS:
+ NOTREACHED();
+ return false;
+ }
+}
+
+TEST_P(ChromeSigninClientSignoutSourceTest, UserSignoutAllowed) {
+ signin_metrics::ProfileSignout signout_source = GetParam();
+
+ TestingProfile::Builder builder;
+ builder.SetGuestSession();
+ std::unique_ptr<TestingProfile> profile = builder.Build();
+
+ CreateClient(profile.get());
+ ASSERT_TRUE(signin_util::IsUserSignoutAllowedForProfile(profile.get()));
+
+ // Verify IdentityManager gets callback indicating sign-out is always allowed.
+ signin_metrics::SignoutDelete delete_metric =
+ signin_metrics::SignoutDelete::kIgnoreMetric;
+ EXPECT_CALL(
+ *client_,
+ SignOutCallback(signout_source, delete_metric,
+ SigninClient::SignoutDecision::ALLOW_SIGNOUT))
+ .Times(1);
+
+ PreSignOut(signout_source, delete_metric);
+}
+
+#if defined(OS_WIN) || defined(OS_LINUX) || defined(OS_CHROMEOS) || \
+ defined(OS_MAC)
+TEST_P(ChromeSigninClientSignoutSourceTest, UserSignoutDisallowed) {
+ signin_metrics::ProfileSignout signout_source = GetParam();
+
+ TestingProfile::Builder builder;
+ builder.SetGuestSession();
+ std::unique_ptr<TestingProfile> profile = builder.Build();
+
+ CreateClient(profile.get());
+
+ ASSERT_TRUE(signin_util::IsUserSignoutAllowedForProfile(profile.get()));
+ signin_util::SetUserSignoutAllowedForProfile(profile.get(), false);
+ ASSERT_FALSE(signin_util::IsUserSignoutAllowedForProfile(profile.get()));
+
+ // Verify IdentityManager gets callback indicating sign-out is disallowed iff
+ // the source of the sign-out is a user-action.
+ SigninClient::SignoutDecision signout_decision =
+ IsSignoutDisallowedByPolicy(profile.get(), signout_source)
+ ? SigninClient::SignoutDecision::DISALLOW_SIGNOUT
+ : SigninClient::SignoutDecision::ALLOW_SIGNOUT;
+ signin_metrics::SignoutDelete delete_metric =
+ signin_metrics::SignoutDelete::kIgnoreMetric;
+ EXPECT_CALL(*client_,
+ SignOutCallback(signout_source, delete_metric, signout_decision))
+ .Times(1);
+
+ PreSignOut(signout_source, delete_metric);
+}
+
+TEST_P(ChromeSigninClientSignoutSourceTest, UserSignoutDisallowedWithSync) {
+ signin_metrics::ProfileSignout signout_source = GetParam();
+
+ TestingProfile::Builder builder;
+ builder.SetGuestSession();
+ std::unique_ptr<TestingProfile> profile = builder.Build();
+
+ CreateClient(profile.get());
+
+ ASSERT_TRUE(signin_util::IsUserSignoutAllowedForProfile(profile.get()));
+ signin_util::SetUserSignoutAllowedForProfile(profile.get(), false);
+ ASSERT_FALSE(signin_util::IsUserSignoutAllowedForProfile(profile.get()));
+
+ // Verify IdentityManager gets callback indicating sign-out is disallowed iff
+ // the source of the sign-out is a user-action.
+ SigninClient::SignoutDecision signout_decision =
+ IsSignoutDisallowedByPolicy(profile.get(), signout_source)
+ ? SigninClient::SignoutDecision::DISALLOW_SIGNOUT
+ : SigninClient::SignoutDecision::ALLOW_SIGNOUT;
+ signin_metrics::SignoutDelete delete_metric =
+ signin_metrics::SignoutDelete::kIgnoreMetric;
+ identity_test_env()->MakePrimaryAccountAvailable("bob@example.com",
+ signin::ConsentLevel::kSync);
+ EXPECT_CALL(*client_,
+ SignOutCallback(signout_source, delete_metric, signout_decision))
+ .Times(1);
+
+ PreSignOut(signout_source, delete_metric);
+}
+
+TEST_P(ChromeSigninClientSignoutSourceTest,
+ UserSignoutDisallowedAccountManagementAccepted) {
+ base::test::ScopedFeatureList features(kAccountPoliciesLoadedWithoutSync);
+ signin_metrics::ProfileSignout signout_source = GetParam();
+
+ TestingProfile::Builder builder;
+ builder.SetGuestSession();
+ std::unique_ptr<TestingProfile> profile = builder.Build();
+
+ CreateClient(profile.get());
+
+ ASSERT_TRUE(signin_util::IsUserSignoutAllowedForProfile(profile.get()));
+ signin_util::SetUserSignoutAllowedForProfile(profile.get(), false);
+ ASSERT_FALSE(signin_util::IsUserSignoutAllowedForProfile(profile.get()));
+
+ // Verify IdentityManager gets callback indicating sign-out is disallowed iff
+ // the source of the sign-out is a user-action.
+ SigninClient::SignoutDecision signout_decision =
+ IsSignoutDisallowedByPolicy(profile.get(), signout_source)
+ ? SigninClient::SignoutDecision::DISALLOW_SIGNOUT
+ : SigninClient::SignoutDecision::ALLOW_SIGNOUT;
+ signin_metrics::SignoutDelete delete_metric =
+ signin_metrics::SignoutDelete::kIgnoreMetric;
+ EXPECT_CALL(*client_,
+ SignOutCallback(signout_source, delete_metric, signout_decision))
+ .Times(1);
+
+ PreSignOut(signout_source, delete_metric);
+}
+#endif
+
+const signin_metrics::ProfileSignout kSignoutSources[] = {
+ signin_metrics::ProfileSignout::SIGNOUT_PREF_CHANGED,
+ signin_metrics::ProfileSignout::GOOGLE_SERVICE_NAME_PATTERN_CHANGED,
+ signin_metrics::ProfileSignout::SIGNIN_PREF_CHANGED_DURING_SIGNIN,
+ signin_metrics::ProfileSignout::USER_CLICKED_SIGNOUT_SETTINGS,
+ signin_metrics::ProfileSignout::ABORT_SIGNIN,
+ signin_metrics::ProfileSignout::SERVER_FORCED_DISABLE,
+ signin_metrics::ProfileSignout::TRANSFER_CREDENTIALS,
+ signin_metrics::ProfileSignout::AUTHENTICATION_FAILED_WITH_FORCE_SIGNIN,
+ signin_metrics::ProfileSignout::USER_TUNED_OFF_SYNC_FROM_DICE_UI,
+ signin_metrics::ProfileSignout::ACCOUNT_REMOVED_FROM_DEVICE,
+ signin_metrics::ProfileSignout::SIGNIN_NOT_ALLOWED_ON_PROFILE_INIT,
+ signin_metrics::ProfileSignout::FORCE_SIGNOUT_ALWAYS_ALLOWED_FOR_TEST,
+ signin_metrics::ProfileSignout::USER_DELETED_ACCOUNT_COOKIES,
+ signin_metrics::ProfileSignout::MOBILE_IDENTITY_CONSISTENCY_ROLLBACK,
+ signin_metrics::ProfileSignout::ACCOUNT_ID_MIGRATION,
+ signin_metrics::ProfileSignout::
+ IOS_ACCOUNT_REMOVED_FROM_DEVICE_AFTER_RESTORE,
+};
+static_assert(base::size(kSignoutSources) ==
+ signin_metrics::ProfileSignout::NUM_PROFILE_SIGNOUT_METRICS,
+ "kSignoutSources should enumerate all ProfileSignout values");
+
+INSTANTIATE_TEST_SUITE_P(AllSignoutSources,
+ ChromeSigninClientSignoutSourceTest,
+ testing::ValuesIn(kSignoutSources));
+
+#endif // !defined(OS_ANDROID)
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
diff --git a/chromium/chrome/browser/signin/chrome_signin_helper.cc b/chromium/chrome/browser/signin/chrome_signin_helper.cc
new file mode 100644
index 00000000000..b85dde43783
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_helper.cc
@@ -0,0 +1,712 @@
+// Copyright 2013 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.
+
+#include "chrome/browser/signin/chrome_signin_helper.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/logging.h"
+#include "base/memory/ref_counted.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/strings/string_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/supports_user_data.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/time/time.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/prefs/incognito_mode_prefs.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_io_data.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/account_reconcilor_factory.h"
+#include "chrome/browser/signin/chrome_signin_client.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/cookie_reminter_factory.h"
+#include "chrome/browser/signin/dice_response_handler.h"
+#include "chrome/browser/signin/header_modification_delegate_impl.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/process_dice_header_delegate_impl.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "chrome/browser/tab_contents/tab_util.h"
+#include "chrome/browser/ui/profile_picker.h"
+#include "chrome/browser/ui/webui/signin/login_ui_service.h"
+#include "chrome/browser/ui/webui/signin/login_ui_service_factory.h"
+#include "chrome/browser/ui/webui/signin/signin_ui_error.h"
+#include "chrome/common/url_constants.h"
+#include "components/account_manager_core/account_manager_facade.h"
+#include "components/signin/core/browser/account_reconcilor.h"
+#include "components/signin/core/browser/cookie_reminter.h"
+#include "components/signin/public/base/account_consistency_method.h"
+#include "components/signin/public/base/signin_buildflags.h"
+#include "components/signin/public/identity_manager/accounts_cookie_mutator.h"
+#include "content/public/browser/browser_task_traits.h"
+#include "content/public/browser/browser_thread.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+#include "net/http/http_response_headers.h"
+
+#if defined(OS_ANDROID)
+#include "chrome/browser/android/signin/signin_bridge.h"
+#include "ui/android/view_android.h"
+#else
+#include "chrome/browser/ui/browser_commands.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/browser_window.h"
+#include "extensions/browser/guest_view/web_view/web_view_renderer_state.h"
+#endif // defined(OS_ANDROID)
+
+#if defined(OS_CHROMEOS)
+#include "chrome/browser/profiles/profile_manager.h"
+#include "components/account_manager_core/chromeos/account_manager_facade_factory.h"
+#endif // defined(OS_CHROMEOS)
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/supervised_user/supervised_user_service.h"
+#include "chrome/browser/supervised_user/supervised_user_service_factory.h"
+#endif // BUILDFLAG(IS_CHROMEOS_ASH)
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+#include "chrome/browser/lacros/account_manager/account_manager_util.h"
+#include "chrome/browser/lacros/account_manager/account_profile_mapper.h"
+#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h"
+#endif
+
+namespace signin {
+
+const void* const kManageAccountsHeaderReceivedUserDataKey =
+ &kManageAccountsHeaderReceivedUserDataKey;
+
+const char kChromeMirrorHeaderSource[] = "Chrome";
+
+namespace {
+
+// Key for RequestDestructionObserverUserData.
+const void* const kRequestDestructionObserverUserDataKey =
+ &kRequestDestructionObserverUserDataKey;
+
+const char kGoogleRemoveLocalAccountResponseHeader[] =
+ "Google-Accounts-RemoveLocalAccount";
+
+const char kRemoveLocalAccountObfuscatedIDAttrName[] = "obfuscatedid";
+
+// TODO(droger): Remove this delay when the Dice implementation is finished on
+// the server side.
+int g_dice_account_reconcilor_blocked_delay_ms = 1000;
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+const char kGoogleSignoutResponseHeader[] = "Google-Accounts-SignOut";
+
+// Refcounted wrapper that facilitates creating and deleting a
+// AccountReconcilor::Lock.
+class AccountReconcilorLockWrapper
+ : public base::RefCountedThreadSafe<AccountReconcilorLockWrapper> {
+ public:
+ explicit AccountReconcilorLockWrapper(
+ const content::WebContents::Getter& web_contents_getter) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ content::WebContents* web_contents = web_contents_getter.Run();
+ if (!web_contents)
+ return;
+ Profile* profile =
+ Profile::FromBrowserContext(web_contents->GetBrowserContext());
+ AccountReconcilor* account_reconcilor =
+ AccountReconcilorFactory::GetForProfile(profile);
+ account_reconcilor_lock_ =
+ std::make_unique<AccountReconcilor::Lock>(account_reconcilor);
+ }
+
+ AccountReconcilorLockWrapper(const AccountReconcilorLockWrapper&) = delete;
+ AccountReconcilorLockWrapper& operator=(const AccountReconcilorLockWrapper&) =
+ delete;
+
+ void DestroyAfterDelay() {
+ // TODO(dcheng): Should ReleaseSoon() support this use case?
+ content::GetUIThreadTaskRunner({})->PostDelayedTask(
+ FROM_HERE,
+ base::BindOnce([](scoped_refptr<AccountReconcilorLockWrapper>) {},
+ base::RetainedRef(this)),
+ base::Milliseconds(g_dice_account_reconcilor_blocked_delay_ms));
+ }
+
+ private:
+ friend class base::RefCountedThreadSafe<AccountReconcilorLockWrapper>;
+ ~AccountReconcilorLockWrapper() {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ }
+
+ std::unique_ptr<AccountReconcilor::Lock> account_reconcilor_lock_;
+};
+
+// Returns true if the account reconcilor needs be be blocked while a Gaia
+// sign-in request is in progress.
+//
+// The account reconcilor must be blocked on all request that may change the
+// Gaia authentication cookies. This includes:
+// * Main frame requests.
+// * XHR requests having Gaia URL as referrer.
+bool ShouldBlockReconcilorForRequest(ChromeRequestAdapter* request) {
+ if (request->GetRequestDestination() ==
+ network::mojom::RequestDestination::kDocument) {
+ return true;
+ }
+
+ return request->IsFetchLikeAPI() &&
+ gaia::IsGaiaSignonRealm(request->GetReferrerOrigin());
+}
+
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+void OnLacrosAccountsAvailableAsSecondaryFetched(
+ AccountProfileMapper* mapper,
+ const base::FilePath& profile_path,
+ const std::vector<account_manager::Account>& accounts) {
+ if (!accounts.empty()) {
+ // Pass in the current profile to signal that the user wants to select a
+ // _secondary_ account for this particular profile.
+ ProfilePicker::Show(
+ ProfilePicker::EntryPoint::kLacrosSelectAvailableAccount, GURL(),
+ profile_path);
+ return;
+ }
+ mapper->ShowAddAccountDialog(profile_path,
+ account_manager::AccountManagerFacade::
+ AccountAdditionSource::kOgbAddAccount,
+ AccountProfileMapper::AddAccountCallback());
+}
+#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
+
+class RequestDestructionObserverUserData : public base::SupportsUserData::Data {
+ public:
+ explicit RequestDestructionObserverUserData(base::OnceClosure closure)
+ : closure_(std::move(closure)) {}
+
+ RequestDestructionObserverUserData(
+ const RequestDestructionObserverUserData&) = delete;
+ RequestDestructionObserverUserData& operator=(
+ const RequestDestructionObserverUserData&) = delete;
+
+ ~RequestDestructionObserverUserData() override { std::move(closure_).Run(); }
+
+ private:
+ base::OnceClosure closure_;
+};
+
+// This user data is used as a marker that a Mirror header was found on the
+// redirect chain. It does not contain any data, its presence is enough to
+// indicate that a header has already be found on the request.
+class ManageAccountsHeaderReceivedUserData
+ : public base::SupportsUserData::Data {};
+
+#if BUILDFLAG(ENABLE_MIRROR)
+// Processes the mirror response header on the UI thread. Currently depending
+// on the value of |header_value|, it either shows the profile avatar menu, or
+// opens an incognito window/tab.
+void ProcessMirrorHeader(
+ ManageAccountsParams manage_accounts_params,
+ const content::WebContents::Getter& web_contents_getter) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+ GAIAServiceType service_type = manage_accounts_params.service_type;
+ DCHECK_NE(GAIA_SERVICE_TYPE_NONE, service_type);
+
+ content::WebContents* web_contents = web_contents_getter.Run();
+ if (!web_contents)
+ return;
+
+ Profile* profile =
+ Profile::FromBrowserContext(web_contents->GetBrowserContext());
+ DCHECK(AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile))
+ << "Gaia should not send the X-Chrome-Manage-Accounts header "
+ << "when Mirror is disabled.";
+ AccountReconcilor* account_reconcilor =
+ AccountReconcilorFactory::GetForProfile(profile);
+ account_reconcilor->OnReceivedManageAccountsResponse(service_type);
+
+#if BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(IS_CHROMEOS_LACROS)
+ signin_metrics::LogAccountReconcilorStateOnGaiaResponse(
+ account_reconcilor->GetState());
+
+ bool should_ignore_guest_webview = true;
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+ // The mirror headers from some guest web views need to be processed.
+ should_ignore_guest_webview =
+ HeaderModificationDelegateImpl::ShouldIgnoreGuestWebViewRequest(
+ web_contents);
+#endif
+
+ Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
+ // Do not do anything if the navigation happened in the "background".
+ if ((!browser || !browser->window()->IsActive()) &&
+ should_ignore_guest_webview) {
+ return;
+ }
+
+ // Record the service type.
+ base::UmaHistogramEnumeration("AccountManager.ManageAccountsServiceType",
+ service_type);
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ // Ignore response to background request from another profile, so dialogs are
+ // not displayed in the wrong profile when using ChromeOS multiprofile mode.
+ if (profile != ProfileManager::GetActiveUserProfile())
+ return;
+#endif // BUILDFLAG(IS_CHROMEOS_ASH)
+
+ // The only allowed operations are:
+ // 1. Going Incognito.
+ // 2. Displaying a reauthentication window: Enterprise GSuite Accounts could
+ // have been forced through an online in-browser sign-in for sensitive
+ // webpages, thereby decreasing their session validity. After their session
+ // expires, they will receive a "Mirror" re-authentication request for all
+ // Google web properties. Another case when this can be triggered is
+ // https://crbug.com/1012649.
+ // 3. Displaying an account addition window: when user clicks "Add another
+ // account" in One Google Bar.
+ // 4. Displaying the Account Manager for managing accounts.
+
+ // 1. Going incognito.
+ if (service_type == GAIA_SERVICE_TYPE_INCOGNITO) {
+ chrome::NewIncognitoWindow(profile);
+ return;
+ }
+
+ // 2. Displaying a reauthentication window
+ if (!manage_accounts_params.email.empty()) {
+ // TODO(https://crbug.com/1226055): enable this for lacros.
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ // Do not display the re-authentication dialog if this event was triggered
+ // by supervision being enabled for an account. In this situation, a
+ // complete signout is required.
+ SupervisedUserService* service =
+ SupervisedUserServiceFactory::GetForProfile(profile);
+ if (service && service->signout_required_after_supervision_enabled()) {
+ return;
+ }
+#endif // BUILDFLAG(IS_CHROMEOS_ASH)
+ // Child users shouldn't get the re-authentication dialog for primary
+ // account. Log out all accounts to re-mint the cookies.
+ // (See the reason below.)
+ signin::IdentityManager* const identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ CoreAccountInfo primary_account =
+ identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
+ if (profile->IsChild() &&
+ gaia::AreEmailsSame(primary_account.email,
+ manage_accounts_params.email)) {
+ identity_manager->GetAccountsCookieMutator()->LogOutAllAccounts(
+ gaia::GaiaSource::kChromeOS, base::DoNothing());
+ return;
+ }
+
+ // The account's cookie is invalid but the cookie has not been removed by
+ // |AccountReconcilor|. Ideally, this should not happen. At this point,
+ // |AccountReconcilor| cannot detect this state because its source of truth
+ // (/ListAccounts) is giving us false positives (claiming an invalid account
+ // to be valid). We need to store that this account's cookie is actually
+ // invalid, so that if/when this account is re-authenticated, we can force a
+ // reconciliation for this account instead of treating it as a no-op.
+ // See https://crbug.com/1012649 for details.
+ AccountInfo maybe_account_info =
+ identity_manager->FindExtendedAccountInfoByEmailAddress(
+ manage_accounts_params.email);
+ if (!maybe_account_info.IsEmpty()) {
+ CookieReminter* const cookie_reminter =
+ CookieReminterFactory::GetForProfile(profile);
+ cookie_reminter->ForceCookieRemintingOnNextTokenUpdate(
+ maybe_account_info);
+ }
+
+ // Display a re-authentication dialog.
+ ::GetAccountManagerFacade(profile->GetPath().value())
+ ->ShowReauthAccountDialog(account_manager::AccountManagerFacade::
+ AccountAdditionSource::kContentAreaReauth,
+ manage_accounts_params.email);
+ return;
+ }
+
+ // 3. Displaying an account addition window.
+ if (service_type == GAIA_SERVICE_TYPE_ADDSESSION) {
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+ AccountProfileMapper* mapper =
+ g_browser_process->profile_manager()->GetAccountProfileMapper();
+ GetAccountsAvailableAsSecondary(
+ mapper, profile->GetPath(),
+ // It's safe to bind raw `mapper`, the callback gets called iff
+ // `mapper` is still valid.
+ base::BindOnce(&OnLacrosAccountsAvailableAsSecondaryFetched, mapper,
+ profile->GetPath()));
+#else
+ ::GetAccountManagerFacade(profile->GetPath().value())
+ ->ShowAddAccountDialog(account_manager::AccountManagerFacade::
+ AccountAdditionSource::kOgbAddAccount);
+#endif
+ return;
+ }
+
+ // 4. Displaying the Account Manager for managing accounts.
+ ::GetAccountManagerFacade(profile->GetPath().value())
+ ->ShowManageAccountsSettings();
+ return;
+
+#elif defined(OS_ANDROID)
+ if (manage_accounts_params.show_consistency_promo) {
+ auto* window = web_contents->GetNativeView()->GetWindowAndroid();
+ if (!window) {
+ // The page is prefetched in the background, ignore the header.
+ // See https://crbug.com/1145031#c5 for details.
+ return;
+ }
+ SigninBridge::OpenAccountPickerBottomSheet(
+ window, manage_accounts_params.continue_url.empty()
+ ? chrome::kChromeUINativeNewTabURL
+ : manage_accounts_params.continue_url);
+ return;
+ }
+ if (service_type == signin::GAIA_SERVICE_TYPE_INCOGNITO) {
+ GURL url(manage_accounts_params.continue_url.empty()
+ ? chrome::kChromeUINativeNewTabURL
+ : manage_accounts_params.continue_url);
+ web_contents->OpenURL(content::OpenURLParams(
+ url, content::Referrer(), WindowOpenDisposition::OFF_THE_RECORD,
+ ui::PAGE_TRANSITION_AUTO_TOPLEVEL, false));
+ } else {
+ signin_metrics::LogAccountReconcilorStateOnGaiaResponse(
+ account_reconcilor->GetState());
+ auto* window = web_contents->GetNativeView()->GetWindowAndroid();
+ if (!window)
+ return;
+ SigninBridge::OpenAccountManagementScreen(window, service_type);
+ }
+#endif // BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(IS_CHROMEOS_LACROS)
+}
+#endif // BUILDFLAG(ENABLE_MIRROR)
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+// Creates a DiceTurnOnSyncHelper.
+void CreateDiceTurnOnSyncHelper(Profile* profile,
+ signin_metrics::AccessPoint access_point,
+ signin_metrics::PromoAction promo_action,
+ signin_metrics::Reason reason,
+ content::WebContents* web_contents,
+ const CoreAccountId& account_id) {
+ DCHECK(profile);
+ Browser* browser = web_contents
+ ? chrome::FindBrowserWithWebContents(web_contents)
+ : chrome::FindBrowserWithProfile(profile);
+ // DiceTurnSyncOnHelper is suicidal (it will kill itself once it finishes
+ // enabling sync).
+ new DiceTurnSyncOnHelper(
+ profile, browser, access_point, promo_action, reason, account_id,
+ DiceTurnSyncOnHelper::SigninAbortedMode::REMOVE_ACCOUNT);
+}
+
+// Shows UI for signin errors.
+void ShowDiceSigninError(Profile* profile,
+ content::WebContents* web_contents,
+ const SigninUIError& error) {
+ DCHECK(profile);
+ Browser* browser = web_contents
+ ? chrome::FindBrowserWithWebContents(web_contents)
+ : chrome::FindBrowserWithProfile(profile);
+ LoginUIServiceFactory::GetForProfile(profile)->DisplayLoginResult(browser,
+ error);
+}
+
+void ProcessDiceHeader(
+ const DiceResponseParams& dice_params,
+ const content::WebContents::Getter& web_contents_getter) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+ content::WebContents* web_contents = web_contents_getter.Run();
+ if (!web_contents)
+ return;
+
+ Profile* profile =
+ Profile::FromBrowserContext(web_contents->GetBrowserContext());
+ DCHECK(!profile->IsOffTheRecord());
+
+ // Ignore Dice response headers if Dice is not enabled.
+ if (!AccountConsistencyModeManager::IsDiceEnabledForProfile(profile))
+ return;
+
+ DiceResponseHandler* dice_response_handler =
+ DiceResponseHandler::GetForProfile(profile);
+ dice_response_handler->ProcessDiceHeader(
+ dice_params,
+ std::make_unique<ProcessDiceHeaderDelegateImpl>(
+ web_contents, base::BindOnce(&CreateDiceTurnOnSyncHelper),
+ base::BindOnce(&ShowDiceSigninError)));
+}
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+#if BUILDFLAG(ENABLE_MIRROR)
+// Looks for the X-Chrome-Manage-Accounts response header, and if found,
+// tries to show the avatar bubble in the browser identified by the
+// child/route id. Must be called on IO thread.
+void ProcessMirrorResponseHeaderIfExists(ResponseAdapter* response,
+ bool is_off_the_record) {
+ CHECK(gaia::IsGaiaSignonRealm(response->GetOrigin()));
+
+ if (!response->IsMainFrame())
+ return;
+
+ const net::HttpResponseHeaders* response_headers = response->GetHeaders();
+ if (!response_headers)
+ return;
+
+ std::string header_value;
+ if (!response_headers->GetNormalizedHeader(kChromeManageAccountsHeader,
+ &header_value)) {
+ return;
+ }
+
+ if (is_off_the_record) {
+ NOTREACHED() << "Gaia should not send the X-Chrome-Manage-Accounts header "
+ << "in incognito.";
+ return;
+ }
+
+ ManageAccountsParams params = BuildManageAccountsParams(header_value);
+ // If the request does not have a response header or if the header contains
+ // garbage, then |service_type| is set to |GAIA_SERVICE_TYPE_NONE|.
+ if (params.service_type == GAIA_SERVICE_TYPE_NONE)
+ return;
+
+ // Only process one mirror header per request (multiple headers on the same
+ // redirect chain are ignored).
+ if (response->GetUserData(kManageAccountsHeaderReceivedUserDataKey)) {
+ LOG(ERROR) << "Multiple X-Chrome-Manage-Accounts headers on a redirect "
+ << "chain, ignoring";
+ return;
+ }
+
+ response->SetUserData(
+ kManageAccountsHeaderReceivedUserDataKey,
+ std::make_unique<ManageAccountsHeaderReceivedUserData>());
+
+ // Post a task even if we are already on the UI thread to avoid making any
+ // requests while processing a throttle event.
+ content::GetUIThreadTaskRunner({})->PostTask(
+ FROM_HERE, base::BindOnce(ProcessMirrorHeader, params,
+ response->GetWebContentsGetter()));
+}
+#endif
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+void ProcessDiceResponseHeaderIfExists(ResponseAdapter* response,
+ bool is_off_the_record) {
+ CHECK(gaia::IsGaiaSignonRealm(response->GetOrigin()));
+
+ if (is_off_the_record)
+ return;
+
+ const net::HttpResponseHeaders* response_headers = response->GetHeaders();
+ if (!response_headers)
+ return;
+
+ std::string header_value;
+ DiceResponseParams params;
+ if (response_headers->GetNormalizedHeader(kDiceResponseHeader,
+ &header_value)) {
+ params = BuildDiceSigninResponseParams(header_value);
+ // The header must be removed for privacy reasons, so that renderers never
+ // have access to the authorization code.
+ response->RemoveHeader(kDiceResponseHeader);
+ } else if (response_headers->GetNormalizedHeader(kGoogleSignoutResponseHeader,
+ &header_value)) {
+ params = BuildDiceSignoutResponseParams(header_value);
+ }
+
+ // If the request does not have a response header or if the header contains
+ // garbage, then |user_intention| is set to |NONE|.
+ if (params.user_intention == DiceAction::NONE)
+ return;
+
+ // Post a task even if we are already on the UI thread to avoid making any
+ // requests while processing a throttle event.
+ content::GetUIThreadTaskRunner({})->PostTask(
+ FROM_HERE, base::BindOnce(ProcessDiceHeader, std::move(params),
+ response->GetWebContentsGetter()));
+}
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+std::string ParseGaiaIdFromRemoveLocalAccountResponseHeader(
+ const net::HttpResponseHeaders* response_headers) {
+ if (!response_headers)
+ return std::string();
+
+ std::string header_value;
+ if (!response_headers->GetNormalizedHeader(
+ kGoogleRemoveLocalAccountResponseHeader, &header_value)) {
+ return std::string();
+ }
+
+ const SigninHeaderHelper::ResponseHeaderDictionary header_dictionary =
+ SigninHeaderHelper::ParseAccountConsistencyResponseHeader(header_value);
+
+ std::string gaia_id;
+ const auto it =
+ header_dictionary.find(kRemoveLocalAccountObfuscatedIDAttrName);
+ if (it != header_dictionary.end()) {
+ // The Gaia ID is wrapped in quotes.
+ base::TrimString(it->second, "\"", &gaia_id);
+ }
+ return gaia_id;
+}
+
+void ProcessRemoveLocalAccountResponseHeaderIfExists(ResponseAdapter* response,
+ bool is_off_the_record) {
+ CHECK(gaia::IsGaiaSignonRealm(response->GetOrigin()));
+
+ if (is_off_the_record)
+ return;
+
+ const std::string gaia_id =
+ ParseGaiaIdFromRemoveLocalAccountResponseHeader(response->GetHeaders());
+
+ if (gaia_id.empty())
+ return;
+
+ content::WebContents* web_contents = response->GetWebContentsGetter().Run();
+ // The tab could have just closed. Technically, it would be possible to
+ // refactor the code to pass around the profile by other means, but this
+ // should be rare enough to be worth supporting.
+ if (!web_contents)
+ return;
+
+ Profile* profile =
+ Profile::FromBrowserContext(web_contents->GetBrowserContext());
+ DCHECK(!profile->IsOffTheRecord());
+
+ IdentityManagerFactory::GetForProfile(profile)
+ ->GetAccountsCookieMutator()
+ ->RemoveLoggedOutAccountByGaiaId(gaia_id);
+}
+
+} // namespace
+
+ChromeRequestAdapter::ChromeRequestAdapter(
+ const GURL& url,
+ const net::HttpRequestHeaders& original_headers,
+ net::HttpRequestHeaders* modified_headers,
+ std::vector<std::string>* headers_to_remove)
+ : RequestAdapter(url,
+ original_headers,
+ modified_headers,
+ headers_to_remove) {}
+
+ChromeRequestAdapter::~ChromeRequestAdapter() = default;
+
+ResponseAdapter::ResponseAdapter() = default;
+
+ResponseAdapter::~ResponseAdapter() = default;
+
+void SetDiceAccountReconcilorBlockDelayForTesting(int delay_ms) {
+ g_dice_account_reconcilor_blocked_delay_ms = delay_ms;
+}
+
+void FixAccountConsistencyRequestHeader(
+ ChromeRequestAdapter* request,
+ const GURL& redirect_url,
+ bool is_off_the_record,
+ int incognito_availibility,
+ AccountConsistencyMethod account_consistency,
+ const std::string& gaia_id,
+ signin::Tribool is_child_account,
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ bool is_secondary_account_addition_allowed,
+#endif
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ bool is_sync_enabled,
+ const std::string& signin_scoped_device_id,
+#endif
+ content_settings::CookieSettings* cookie_settings) {
+ if (is_off_the_record)
+ return; // Account consistency is disabled in incognito.
+
+ // If new url is eligible to have the header, add it, otherwise remove it.
+
+// Mirror header:
+#if BUILDFLAG(ENABLE_MIRROR)
+ int profile_mode_mask = PROFILE_MODE_DEFAULT;
+ if (incognito_availibility ==
+ static_cast<int>(IncognitoModePrefs::Availability::kDisabled) ||
+ IncognitoModePrefs::ArePlatformParentalControlsEnabled()) {
+ profile_mode_mask |= PROFILE_MODE_INCOGNITO_DISABLED;
+ }
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ if (!is_secondary_account_addition_allowed) {
+ account_consistency = AccountConsistencyMethod::kMirror;
+ // Can't add new accounts.
+ profile_mode_mask |= PROFILE_MODE_ADD_ACCOUNT_DISABLED;
+ }
+#endif
+
+ AppendOrRemoveMirrorRequestHeader(
+ request, redirect_url, gaia_id, is_child_account, account_consistency,
+ cookie_settings, profile_mode_mask, kChromeMirrorHeaderSource,
+ /*force_account_consistency=*/false);
+#endif // BUILDFLAG(ENABLE_MIRROR)
+
+// Dice header:
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ bool dice_header_added = AppendOrRemoveDiceRequestHeader(
+ request, redirect_url, gaia_id, is_sync_enabled, account_consistency,
+ cookie_settings, signin_scoped_device_id);
+
+ // Block the AccountReconcilor while the Dice requests are in flight. This
+ // allows the DiceReponseHandler to process the response before the reconcilor
+ // starts.
+ if (dice_header_added && ShouldBlockReconcilorForRequest(request)) {
+ auto lock_wrapper = base::MakeRefCounted<AccountReconcilorLockWrapper>(
+ request->GetWebContentsGetter());
+ // On destruction of the request |lock_wrapper| will be released.
+ request->SetDestructionCallback(base::BindOnce(
+ &AccountReconcilorLockWrapper::DestroyAfterDelay, lock_wrapper));
+ }
+#endif
+}
+
+void ProcessAccountConsistencyResponseHeaders(ResponseAdapter* response,
+ const GURL& redirect_url,
+ bool is_off_the_record) {
+ if (!gaia::IsGaiaSignonRealm(response->GetOrigin()))
+ return;
+
+#if BUILDFLAG(ENABLE_MIRROR)
+ // See if the response contains the X-Chrome-Manage-Accounts header. If so
+ // show the profile avatar bubble so that user can complete signin/out
+ // action the native UI.
+ ProcessMirrorResponseHeaderIfExists(response, is_off_the_record);
+#endif
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ // Process the Dice header: on sign-in, exchange the authorization code for a
+ // refresh token, on sign-out just follow the sign-out URL.
+ ProcessDiceResponseHeaderIfExists(response, is_off_the_record);
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+ if (base::FeatureList::IsEnabled(kProcessGaiaRemoveLocalAccountHeader)) {
+ ProcessRemoveLocalAccountResponseHeaderIfExists(response,
+ is_off_the_record);
+ }
+}
+
+std::string ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting(
+ const net::HttpResponseHeaders* response_headers) {
+ return ParseGaiaIdFromRemoveLocalAccountResponseHeader(response_headers);
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/chrome_signin_helper.h b/chromium/chrome/browser/signin/chrome_signin_helper.h
new file mode 100644
index 00000000000..7d6ea870458
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_helper.h
@@ -0,0 +1,129 @@
+// Copyright 2013 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_SIGNIN_CHROME_SIGNIN_HELPER_H_
+#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_HELPER_H_
+
+#include <memory>
+#include <string>
+
+#include "base/supports_user_data.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/prefs/incognito_mode_prefs.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "content/public/browser/web_contents.h"
+#include "services/network/public/mojom/fetch_api.mojom-shared.h"
+
+namespace content_settings {
+class CookieSettings;
+}
+
+namespace net {
+class HttpResponseHeaders;
+}
+
+class GURL;
+
+// Utility functions for handling Chrome/Gaia headers during signin process.
+// Chrome identity should always stay in sync with Gaia identity. Therefore
+// Chrome needs to send Gaia special header for requests from a connected
+// profile, so that Gaia can modify its response accordingly and let Chrome
+// handle signin accordingly.
+namespace signin {
+
+enum class Tribool;
+
+// Key for ManageAccountsHeaderReceivedUserData. Exposed for testing.
+extern const void* const kManageAccountsHeaderReceivedUserDataKey;
+
+// The source to use when constructing the Mirror header.
+extern const char kChromeMirrorHeaderSource[];
+
+class ChromeRequestAdapter : public RequestAdapter {
+ public:
+ ChromeRequestAdapter(const GURL& url,
+ const net::HttpRequestHeaders& original_headers,
+ net::HttpRequestHeaders* modified_headers,
+ std::vector<std::string>* headers_to_remove);
+
+ ChromeRequestAdapter(const ChromeRequestAdapter&) = delete;
+ ChromeRequestAdapter& operator=(const ChromeRequestAdapter&) = delete;
+
+ ~ChromeRequestAdapter() override;
+
+ virtual content::WebContents::Getter GetWebContentsGetter() const = 0;
+
+ virtual network::mojom::RequestDestination GetRequestDestination() const = 0;
+
+ virtual bool IsFetchLikeAPI() const = 0;
+
+ virtual GURL GetReferrerOrigin() const = 0;
+
+ // Associate a callback with this request which will be executed when the
+ // request is complete (including any redirects). If a callback was already
+ // registered this function does nothing.
+ virtual void SetDestructionCallback(base::OnceClosure closure) = 0;
+};
+
+class ResponseAdapter {
+ public:
+ ResponseAdapter();
+
+ ResponseAdapter(const ResponseAdapter&) = delete;
+ ResponseAdapter& operator=(const ResponseAdapter&) = delete;
+
+ virtual ~ResponseAdapter();
+
+ virtual content::WebContents::Getter GetWebContentsGetter() const = 0;
+ virtual bool IsMainFrame() const = 0;
+ virtual GURL GetOrigin() const = 0;
+ virtual const net::HttpResponseHeaders* GetHeaders() const = 0;
+ virtual void RemoveHeader(const std::string& name) = 0;
+
+ virtual base::SupportsUserData::Data* GetUserData(const void* key) const = 0;
+ virtual void SetUserData(
+ const void* key,
+ std::unique_ptr<base::SupportsUserData::Data> data) = 0;
+};
+
+// When Dice is enabled, the AccountReconcilor is blocked for a short delay
+// after sending requests to Gaia. Exposed for testing.
+void SetDiceAccountReconcilorBlockDelayForTesting(int delay_ms);
+
+// Adds an account consistency header to Gaia requests from a connected profile,
+// with the exception of requests from gaia webview.
+// Removes the header if it is already in the headers but should not be there.
+void FixAccountConsistencyRequestHeader(
+ ChromeRequestAdapter* request,
+ const GURL& redirect_url,
+ bool is_off_the_record,
+ int incognito_availibility,
+ AccountConsistencyMethod account_consistency,
+ const std::string& gaia_id,
+ signin::Tribool is_child_account,
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ bool is_secondary_account_addition_allowed,
+#endif
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ bool is_sync_enabled,
+ const std::string& signin_scoped_device_id,
+#endif
+ content_settings::CookieSettings* cookie_settings);
+
+// Processes account consistency response headers (X-Chrome-Manage-Accounts and
+// Dice). |redirect_url| is empty if the request is not a redirect.
+void ProcessAccountConsistencyResponseHeaders(ResponseAdapter* response,
+ const GURL& redirect_url,
+ bool is_off_the_record);
+
+// Parses and returns an account ID (Gaia ID) from HTTP response header
+// Google-Accounts-RemoveLocalAccount. Returns an empty string if parsing
+// failed. Exposed for testing purposes.
+std::string ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting(
+ const net::HttpResponseHeaders* response_headers);
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_HELPER_H_
diff --git a/chromium/chrome/browser/signin/chrome_signin_helper_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_helper_unittest.cc
new file mode 100644
index 00000000000..8a5757aabe3
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_helper_unittest.cc
@@ -0,0 +1,182 @@
+// Copyright 2017 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.
+
+#include "chrome/browser/signin/chrome_signin_helper.h"
+
+#include <memory>
+#include <string>
+
+#include "base/run_loop.h"
+#include "base/strings/stringprintf.h"
+#include "build/chromeos_buildflags.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "components/signin/public/base/signin_buildflags.h"
+#include "content/public/test/browser_task_environment.h"
+#include "net/http/http_response_headers.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "net/url_request/url_request.h"
+#include "net/url_request/url_request_context.h"
+#include "net/url_request/url_request_filter.h"
+#include "net/url_request/url_request_interceptor.h"
+#include "net/url_request/url_request_test_job.h"
+#include "net/url_request/url_request_test_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace {
+
+#if BUILDFLAG(ENABLE_MIRROR) || BUILDFLAG(IS_CHROMEOS_LACROS)
+const char kChromeManageAccountsHeader[] = "X-Chrome-Manage-Accounts";
+const char kMirrorAction[] = "action=ADDSESSION";
+#endif
+
+// URLRequestInterceptor adding a account consistency response header to Gaia
+// responses.
+class TestRequestInterceptor : public net::URLRequestInterceptor {
+ public:
+ explicit TestRequestInterceptor(const std::string& header_name,
+ const std::string& header_value)
+ : header_name_(header_name), header_value_(header_value) {}
+ ~TestRequestInterceptor() override = default;
+
+ private:
+ std::unique_ptr<net::URLRequestJob> MaybeInterceptRequest(
+ net::URLRequest* request) const override {
+ std::string response_headers =
+ base::StringPrintf("HTTP/1.1 200 OK\n\n%s: %s\n", header_name_.c_str(),
+ header_value_.c_str());
+ return std::make_unique<net::URLRequestTestJob>(request, response_headers,
+ "", true);
+ }
+
+ const std::string header_name_;
+ const std::string header_value_;
+};
+
+class TestResponseAdapter : public signin::ResponseAdapter,
+ public base::SupportsUserData {
+ public:
+ TestResponseAdapter(const std::string& header_name,
+ const std::string& header_value,
+ bool is_main_frame)
+ : is_main_frame_(is_main_frame),
+ headers_(new net::HttpResponseHeaders(std::string())) {
+ headers_->SetHeader(header_name, header_value);
+ }
+
+ TestResponseAdapter(const TestResponseAdapter&) = delete;
+ TestResponseAdapter& operator=(const TestResponseAdapter&) = delete;
+
+ ~TestResponseAdapter() override {}
+
+ content::WebContents::Getter GetWebContentsGetter() const override {
+ return base::BindRepeating(
+ []() -> content::WebContents* { return nullptr; });
+ }
+ bool IsMainFrame() const override { return is_main_frame_; }
+ GURL GetOrigin() const override {
+ return GURL("https://accounts.google.com");
+ }
+ const net::HttpResponseHeaders* GetHeaders() const override {
+ return headers_.get();
+ }
+
+ void RemoveHeader(const std::string& name) override {
+ headers_->RemoveHeader(name);
+ }
+
+ base::SupportsUserData::Data* GetUserData(const void* key) const override {
+ return base::SupportsUserData::GetUserData(key);
+ }
+
+ void SetUserData(
+ const void* key,
+ std::unique_ptr<base::SupportsUserData::Data> data) override {
+ return base::SupportsUserData::SetUserData(key, std::move(data));
+ }
+
+ private:
+ bool is_main_frame_;
+ scoped_refptr<net::HttpResponseHeaders> headers_;
+};
+
+} // namespace
+
+class ChromeSigninHelperTest : public testing::Test {
+ protected:
+ ChromeSigninHelperTest()
+ : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP) {}
+
+ ~ChromeSigninHelperTest() override = default;
+
+ content::BrowserTaskEnvironment task_environment_;
+ std::unique_ptr<net::TestDelegate> test_request_delegate_;
+};
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+// Tests that Dice response headers are removed after being processed.
+TEST_F(ChromeSigninHelperTest, RemoveDiceSigninHeader) {
+ // Process the header.
+ TestResponseAdapter adapter(signin::kDiceResponseHeader, "Foo",
+ /*is_main_frame=*/false);
+ signin::ProcessAccountConsistencyResponseHeaders(&adapter, GURL(),
+ false /* is_incognito */);
+
+ // Check that the header has been removed.
+ EXPECT_FALSE(adapter.GetHeaders()->HasHeader(signin::kDiceResponseHeader));
+}
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+#if BUILDFLAG(ENABLE_MIRROR) || BUILDFLAG(IS_CHROMEOS_LACROS)
+// Tests that user data is set on Mirror requests.
+TEST_F(ChromeSigninHelperTest, MirrorMainFrame) {
+ // Process the header.
+ TestResponseAdapter response_adapter(kChromeManageAccountsHeader,
+ kMirrorAction,
+ /*is_main_frame=*/true);
+ signin::ProcessAccountConsistencyResponseHeaders(&response_adapter, GURL(),
+ false /* is_incognito */);
+ // Check that the header has not been removed.
+ EXPECT_TRUE(
+ response_adapter.GetHeaders()->HasHeader(kChromeManageAccountsHeader));
+ // Request was flagged with the user data.
+ EXPECT_TRUE(response_adapter.GetUserData(
+ signin::kManageAccountsHeaderReceivedUserDataKey));
+}
+
+// Tests that user data is not set on Mirror requests for sub frames.
+TEST_F(ChromeSigninHelperTest, MirrorSubFrame) {
+ // Process the header.
+ TestResponseAdapter response_adapter(kChromeManageAccountsHeader,
+ kMirrorAction,
+ /*is_main_frame=*/false);
+ signin::ProcessAccountConsistencyResponseHeaders(&response_adapter, GURL(),
+ false /* is_incognito */);
+ // Request was not flagged with the user data.
+ EXPECT_FALSE(response_adapter.GetUserData(
+ signin::kManageAccountsHeaderReceivedUserDataKey));
+}
+#endif // BUILDFLAG(ENABLE_MIRROR) || BUILDFLAG(IS_CHROMEOS_LACROS)
+
+TEST_F(ChromeSigninHelperTest,
+ ParseGaiaIdFromRemoveLocalAccountResponseHeader) {
+ EXPECT_EQ("123456",
+ signin::ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting(
+ TestResponseAdapter("Google-Accounts-RemoveLocalAccount",
+ "obfuscatedid=\"123456\"",
+ /*is_main_frame=*/false)
+ .GetHeaders()));
+ EXPECT_EQ("123456",
+ signin::ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting(
+ TestResponseAdapter("Google-Accounts-RemoveLocalAccount",
+ "obfuscatedid=\"123456\",foo=\"bar\"",
+ /*is_main_frame=*/false)
+ .GetHeaders()));
+ EXPECT_EQ(
+ "",
+ signin::ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting(
+ TestResponseAdapter("Google-Accounts-RemoveLocalAccount", "malformed",
+ /*is_main_frame=*/false)
+ .GetHeaders()));
+}
diff --git a/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.cc b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.cc
new file mode 100644
index 00000000000..d4b8d911a9e
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.cc
@@ -0,0 +1,551 @@
+// Copyright 2018 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.
+
+#include "chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h"
+
+#include "base/barrier_closure.h"
+#include "base/bind.h"
+#include "base/memory/ptr_util.h"
+#include "base/memory/raw_ptr.h"
+#include "base/supports_user_data.h"
+#include "build/build_config.h"
+#include "build/buildflag.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/chrome_signin_helper.h"
+#include "chrome/browser/signin/header_modification_delegate.h"
+#include "chrome/browser/signin/header_modification_delegate_impl.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/render_process_host.h"
+#include "extensions/browser/guest_view/web_view/web_view_renderer_state.h"
+#include "extensions/buildflags/buildflags.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "net/base/net_errors.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/mojom/early_hints.mojom.h"
+#include "services/network/public/mojom/fetch_api.mojom-shared.h"
+#include "services/network/public/mojom/url_loader.mojom.h"
+#include "services/network/public/mojom/url_loader_factory.mojom.h"
+#include "services/network/public/mojom/url_response_head.mojom.h"
+
+#if defined(OS_ANDROID)
+#include "chrome/browser/android/tab_android.h"
+#include "chrome/browser/android/tab_web_contents_delegate_android.h"
+#endif
+
+namespace signin {
+
+namespace {
+
+// User data key for BrowserContextData.
+const void* const kBrowserContextUserDataKey = &kBrowserContextUserDataKey;
+
+// Owns all of the ProxyingURLLoaderFactorys for a given Profile.
+class BrowserContextData : public base::SupportsUserData::Data {
+ public:
+ BrowserContextData(const BrowserContextData&) = delete;
+ BrowserContextData& operator=(const BrowserContextData&) = delete;
+
+ ~BrowserContextData() override {}
+
+ static void StartProxying(
+ Profile* profile,
+ content::WebContents::Getter web_contents_getter,
+ mojo::PendingReceiver<network::mojom::URLLoaderFactory> receiver,
+ mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory) {
+ auto* self = static_cast<BrowserContextData*>(
+ profile->GetUserData(kBrowserContextUserDataKey));
+ if (!self) {
+ self = new BrowserContextData();
+ profile->SetUserData(kBrowserContextUserDataKey, base::WrapUnique(self));
+ }
+
+#if defined(OS_ANDROID)
+ bool is_custom_tab = false;
+ content::WebContents* web_contents = web_contents_getter.Run();
+ if (web_contents) {
+ auto* delegate =
+ TabAndroid::FromWebContents(web_contents)
+ ? static_cast<android::TabWebContentsDelegateAndroid*>(
+ web_contents->GetDelegate())
+ : nullptr;
+ is_custom_tab = delegate && delegate->IsCustomTab();
+ }
+ auto delegate = std::make_unique<HeaderModificationDelegateImpl>(
+ profile, /*incognito_enabled=*/!is_custom_tab);
+#else
+ auto delegate = std::make_unique<HeaderModificationDelegateImpl>(profile);
+#endif
+ auto proxy = std::make_unique<ProxyingURLLoaderFactory>(
+ std::move(delegate), std::move(web_contents_getter),
+ std::move(receiver), std::move(target_factory),
+ base::BindOnce(&BrowserContextData::RemoveProxy,
+ self->weak_factory_.GetWeakPtr()));
+ self->proxies_.emplace(std::move(proxy));
+ }
+
+ void RemoveProxy(ProxyingURLLoaderFactory* proxy) {
+ auto it = proxies_.find(proxy);
+ DCHECK(it != proxies_.end());
+ proxies_.erase(it);
+ }
+
+ private:
+ BrowserContextData() {}
+
+ std::set<std::unique_ptr<ProxyingURLLoaderFactory>, base::UniquePtrComparator>
+ proxies_;
+
+ base::WeakPtrFactory<BrowserContextData> weak_factory_{this};
+};
+
+} // namespace
+
+class ProxyingURLLoaderFactory::InProgressRequest
+ : public network::mojom::URLLoader,
+ public network::mojom::URLLoaderClient,
+ public base::SupportsUserData {
+ public:
+ InProgressRequest(
+ ProxyingURLLoaderFactory* factory,
+ mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver,
+ int32_t request_id,
+ uint32_t options,
+ const network::ResourceRequest& request,
+ mojo::PendingRemote<network::mojom::URLLoaderClient> client,
+ const net::MutableNetworkTrafficAnnotationTag& traffic_annotation);
+
+ InProgressRequest(const InProgressRequest&) = delete;
+ InProgressRequest& operator=(const InProgressRequest&) = delete;
+
+ ~InProgressRequest() override {
+ if (destruction_callback_)
+ std::move(destruction_callback_).Run();
+ }
+
+ // network::mojom::URLLoader:
+ void FollowRedirect(
+ const std::vector<std::string>& removed_headers,
+ const net::HttpRequestHeaders& modified_headers,
+ const net::HttpRequestHeaders& modified_cors_exempt_headers,
+ const absl::optional<GURL>& new_url) override;
+
+ void SetPriority(net::RequestPriority priority,
+ int32_t intra_priority_value) override {
+ target_loader_->SetPriority(priority, intra_priority_value);
+ }
+
+ void PauseReadingBodyFromNet() override {
+ target_loader_->PauseReadingBodyFromNet();
+ }
+
+ void ResumeReadingBodyFromNet() override {
+ target_loader_->ResumeReadingBodyFromNet();
+ }
+
+ // network::mojom::URLLoaderClient:
+ void OnReceiveEarlyHints(network::mojom::EarlyHintsPtr early_hints) override {
+ target_client_->OnReceiveEarlyHints(std::move(early_hints));
+ }
+ void OnReceiveResponse(network::mojom::URLResponseHeadPtr head) override;
+ void OnReceiveRedirect(const net::RedirectInfo& redirect_info,
+ network::mojom::URLResponseHeadPtr head) override;
+
+ void OnUploadProgress(int64_t current_position,
+ int64_t total_size,
+ OnUploadProgressCallback callback) override {
+ target_client_->OnUploadProgress(current_position, total_size,
+ std::move(callback));
+ }
+
+ void OnReceiveCachedMetadata(mojo_base::BigBuffer data) override {
+ target_client_->OnReceiveCachedMetadata(std::move(data));
+ }
+
+ void OnTransferSizeUpdated(int32_t transfer_size_diff) override {
+ target_client_->OnTransferSizeUpdated(transfer_size_diff);
+ }
+
+ void OnStartLoadingResponseBody(
+ mojo::ScopedDataPipeConsumerHandle body) override {
+ target_client_->OnStartLoadingResponseBody(std::move(body));
+ }
+
+ void OnComplete(const network::URLLoaderCompletionStatus& status) override {
+ target_client_->OnComplete(status);
+ }
+
+ private:
+ class ProxyRequestAdapter;
+ class ProxyResponseAdapter;
+
+ void OnBindingsClosed() {
+ // Destroys |this|.
+ factory_->RemoveRequest(this);
+ }
+
+ // Back pointer to the factory which owns this class.
+ const raw_ptr<ProxyingURLLoaderFactory> factory_;
+
+ // Information about the current request.
+ GURL request_url_;
+ GURL response_url_;
+ GURL referrer_origin_;
+ net::HttpRequestHeaders headers_;
+ net::HttpRequestHeaders cors_exempt_headers_;
+ net::RedirectInfo redirect_info_;
+ const network::mojom::RequestDestination request_destination_;
+ const bool is_main_frame_;
+ const bool is_fetch_like_api_;
+
+ base::OnceClosure destruction_callback_;
+
+ // Messages received by |client_receiver_| are forwarded to |target_client_|.
+ mojo::Receiver<network::mojom::URLLoaderClient> client_receiver_{this};
+ mojo::Remote<network::mojom::URLLoaderClient> target_client_;
+
+ // Messages received by |loader_receiver_| are forwarded to |target_loader_|.
+ mojo::Receiver<network::mojom::URLLoader> loader_receiver_;
+ mojo::Remote<network::mojom::URLLoader> target_loader_;
+};
+
+class ProxyingURLLoaderFactory::InProgressRequest::ProxyRequestAdapter
+ : public ChromeRequestAdapter {
+ public:
+ // Does not take |modified_cors_exempt_headers| just because we don't have a
+ // use-case to modify it in this class now.
+ ProxyRequestAdapter(InProgressRequest* in_progress_request,
+ const net::HttpRequestHeaders& original_headers,
+ net::HttpRequestHeaders* modified_headers,
+ std::vector<std::string>* removed_headers)
+ : ChromeRequestAdapter(in_progress_request->request_url_,
+ original_headers,
+ modified_headers,
+ removed_headers),
+ in_progress_request_(in_progress_request) {
+ DCHECK(in_progress_request_);
+ }
+
+ ProxyRequestAdapter(const ProxyRequestAdapter&) = delete;
+ ProxyRequestAdapter& operator=(const ProxyRequestAdapter&) = delete;
+
+ ~ProxyRequestAdapter() override = default;
+
+ content::WebContents::Getter GetWebContentsGetter() const override {
+ return in_progress_request_->factory_->web_contents_getter_;
+ }
+
+ network::mojom::RequestDestination GetRequestDestination() const override {
+ return in_progress_request_->request_destination_;
+ }
+
+ bool IsFetchLikeAPI() const override {
+ return in_progress_request_->is_fetch_like_api_;
+ }
+
+ GURL GetReferrerOrigin() const override {
+ return in_progress_request_->referrer_origin_;
+ }
+
+ void SetDestructionCallback(base::OnceClosure closure) override {
+ if (!in_progress_request_->destruction_callback_)
+ in_progress_request_->destruction_callback_ = std::move(closure);
+ }
+
+ private:
+ const raw_ptr<InProgressRequest> in_progress_request_;
+};
+
+class ProxyingURLLoaderFactory::InProgressRequest::ProxyResponseAdapter
+ : public ResponseAdapter {
+ public:
+ ProxyResponseAdapter(InProgressRequest* in_progress_request,
+ net::HttpResponseHeaders* headers)
+ : in_progress_request_(in_progress_request), headers_(headers) {
+ DCHECK(in_progress_request_);
+ DCHECK(headers_);
+ }
+
+ ProxyResponseAdapter(const ProxyResponseAdapter&) = delete;
+ ProxyResponseAdapter& operator=(const ProxyResponseAdapter&) = delete;
+
+ ~ProxyResponseAdapter() override = default;
+
+ // signin::ResponseAdapter
+ content::WebContents::Getter GetWebContentsGetter() const override {
+ return in_progress_request_->factory_->web_contents_getter_;
+ }
+
+ bool IsMainFrame() const override {
+ return in_progress_request_->is_main_frame_;
+ }
+
+ GURL GetOrigin() const override {
+ return in_progress_request_->response_url_.DeprecatedGetOriginAsURL();
+ }
+
+ const net::HttpResponseHeaders* GetHeaders() const override {
+ return headers_;
+ }
+
+ void RemoveHeader(const std::string& name) override {
+ headers_->RemoveHeader(name);
+ }
+
+ base::SupportsUserData::Data* GetUserData(const void* key) const override {
+ return in_progress_request_->GetUserData(key);
+ }
+
+ void SetUserData(
+ const void* key,
+ std::unique_ptr<base::SupportsUserData::Data> data) override {
+ in_progress_request_->SetUserData(key, std::move(data));
+ }
+
+ private:
+ const raw_ptr<InProgressRequest> in_progress_request_;
+ const raw_ptr<net::HttpResponseHeaders> headers_;
+};
+
+ProxyingURLLoaderFactory::InProgressRequest::InProgressRequest(
+ ProxyingURLLoaderFactory* factory,
+ mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver,
+ int32_t request_id,
+ uint32_t options,
+ const network::ResourceRequest& request,
+ mojo::PendingRemote<network::mojom::URLLoaderClient> client,
+ const net::MutableNetworkTrafficAnnotationTag& traffic_annotation)
+ : factory_(factory),
+ request_url_(request.url),
+ response_url_(request.url),
+ referrer_origin_(request.referrer.DeprecatedGetOriginAsURL()),
+ request_destination_(request.destination),
+ is_main_frame_(request.is_main_frame),
+ is_fetch_like_api_(request.is_fetch_like_api),
+ target_client_(std::move(client)),
+ loader_receiver_(this, std::move(loader_receiver)) {
+ mojo::PendingRemote<network::mojom::URLLoaderClient> proxy_client =
+ client_receiver_.BindNewPipeAndPassRemote();
+
+ net::HttpRequestHeaders modified_headers;
+ std::vector<std::string> removed_headers;
+ ProxyRequestAdapter adapter(this, request.headers, &modified_headers,
+ &removed_headers);
+ factory_->delegate_->ProcessRequest(&adapter, GURL() /* redirect_url */);
+
+ if (modified_headers.IsEmpty() && removed_headers.empty()) {
+ factory_->target_factory_->CreateLoaderAndStart(
+ target_loader_.BindNewPipeAndPassReceiver(), request_id, options,
+ request, std::move(proxy_client), traffic_annotation);
+
+ // We need to keep a full copy of the request headers in case there is a
+ // redirect and the request headers need to be modified again.
+ headers_.CopyFrom(request.headers);
+ cors_exempt_headers_.CopyFrom(request.cors_exempt_headers);
+ } else {
+ network::ResourceRequest request_copy = request;
+ request_copy.headers.MergeFrom(modified_headers);
+ for (const std::string& name : removed_headers) {
+ request_copy.headers.RemoveHeader(name);
+ request_copy.cors_exempt_headers.RemoveHeader(name);
+ }
+
+ factory_->target_factory_->CreateLoaderAndStart(
+ target_loader_.BindNewPipeAndPassReceiver(), request_id, options,
+ request_copy, std::move(proxy_client), traffic_annotation);
+
+ headers_.Swap(&request_copy.headers);
+ cors_exempt_headers_.Swap(&request_copy.cors_exempt_headers);
+ }
+
+ base::RepeatingClosure closure = base::BarrierClosure(
+ 2, base::BindOnce(&InProgressRequest::OnBindingsClosed,
+ base::Unretained(this)));
+ loader_receiver_.set_disconnect_handler(closure);
+ client_receiver_.set_disconnect_handler(closure);
+}
+
+void ProxyingURLLoaderFactory::InProgressRequest::FollowRedirect(
+ const std::vector<std::string>& removed_headers_ext,
+ const net::HttpRequestHeaders& modified_headers_ext,
+ const net::HttpRequestHeaders& modified_cors_exempt_headers_ext,
+ const absl::optional<GURL>& opt_new_url) {
+ std::vector<std::string> removed_headers = removed_headers_ext;
+ net::HttpRequestHeaders modified_headers = modified_headers_ext;
+ net::HttpRequestHeaders modified_cors_exempt_headers =
+ modified_cors_exempt_headers_ext;
+ ProxyRequestAdapter adapter(this, headers_, &modified_headers,
+ &removed_headers);
+ factory_->delegate_->ProcessRequest(&adapter, redirect_info_.new_url);
+
+ headers_.MergeFrom(modified_headers);
+ cors_exempt_headers_.MergeFrom(modified_cors_exempt_headers);
+ for (const std::string& name : removed_headers) {
+ headers_.RemoveHeader(name);
+ cors_exempt_headers_.RemoveHeader(name);
+ }
+
+ target_loader_->FollowRedirect(removed_headers, modified_headers,
+ modified_cors_exempt_headers, opt_new_url);
+
+ request_url_ = redirect_info_.new_url;
+ referrer_origin_ =
+ GURL(redirect_info_.new_referrer).DeprecatedGetOriginAsURL();
+}
+
+void ProxyingURLLoaderFactory::InProgressRequest::OnReceiveResponse(
+ network::mojom::URLResponseHeadPtr head) {
+ // Even though |head| is const we can get a non-const pointer to the headers
+ // and modifications we made are passed to the target client.
+ ProxyResponseAdapter adapter(this, head->headers.get());
+ factory_->delegate_->ProcessResponse(&adapter, GURL() /* redirect_url */);
+ target_client_->OnReceiveResponse(std::move(head));
+}
+
+void ProxyingURLLoaderFactory::InProgressRequest::OnReceiveRedirect(
+ const net::RedirectInfo& redirect_info,
+ network::mojom::URLResponseHeadPtr head) {
+ // Even though |head| is const we can get a non-const pointer to the headers
+ // and modifications we made are passed to the target client.
+ ProxyResponseAdapter adapter(this, head->headers.get());
+ factory_->delegate_->ProcessResponse(&adapter, redirect_info.new_url);
+ target_client_->OnReceiveRedirect(redirect_info, std::move(head));
+
+ // The request URL returned by ProxyResponseAdapter::GetOrigin() is updated
+ // immediately but the URL and referrer
+ redirect_info_ = redirect_info;
+ response_url_ = redirect_info.new_url;
+}
+
+ProxyingURLLoaderFactory::ProxyingURLLoaderFactory(
+ std::unique_ptr<HeaderModificationDelegate> delegate,
+ content::WebContents::Getter web_contents_getter,
+ mojo::PendingReceiver<network::mojom::URLLoaderFactory> loader_receiver,
+ mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory,
+ DisconnectCallback on_disconnect) {
+ DCHECK(proxy_receivers_.empty());
+ DCHECK(!target_factory_.is_bound());
+ DCHECK(!delegate_);
+ DCHECK(!web_contents_getter_);
+ DCHECK(!on_disconnect_);
+
+ delegate_ = std::move(delegate);
+ web_contents_getter_ = std::move(web_contents_getter);
+ on_disconnect_ = std::move(on_disconnect);
+
+ target_factory_.Bind(std::move(target_factory));
+ target_factory_.set_disconnect_handler(base::BindOnce(
+ &ProxyingURLLoaderFactory::OnTargetFactoryError, base::Unretained(this)));
+
+ proxy_receivers_.Add(this, std::move(loader_receiver));
+ proxy_receivers_.set_disconnect_handler(base::BindRepeating(
+ &ProxyingURLLoaderFactory::OnProxyBindingError, base::Unretained(this)));
+}
+
+ProxyingURLLoaderFactory::~ProxyingURLLoaderFactory() = default;
+
+// static
+bool ProxyingURLLoaderFactory::MaybeProxyRequest(
+ content::RenderFrameHost* render_frame_host,
+ bool is_navigation,
+ const url::Origin& request_initiator,
+ mojo::PendingReceiver<network::mojom::URLLoaderFactory>* factory_receiver) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+ // Navigation requests are handled using signin::URLLoaderThrottle.
+ if (is_navigation)
+ return false;
+
+ if (!render_frame_host)
+ return false;
+
+ // This proxy should only be installed for subresource requests from a frame
+ // that is rendering the GAIA signon realm.
+ if (!gaia::IsGaiaSignonRealm(request_initiator.GetURL()))
+ return false;
+
+ auto* web_contents =
+ content::WebContents::FromRenderFrameHost(render_frame_host);
+ auto* profile =
+ Profile::FromBrowserContext(web_contents->GetBrowserContext());
+ if (profile->IsOffTheRecord())
+ return false;
+
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+ // Most requests from guest web views are ignored.
+ if (HeaderModificationDelegateImpl::ShouldIgnoreGuestWebViewRequest(
+ web_contents)) {
+ return false;
+ }
+#endif
+
+ auto proxied_receiver = std::move(*factory_receiver);
+ // TODO(crbug.com/955171): Replace this with PendingRemote.
+ mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory_remote;
+ *factory_receiver = target_factory_remote.InitWithNewPipeAndPassReceiver();
+
+ auto web_contents_getter =
+ base::BindRepeating(&content::WebContents::FromFrameTreeNodeId,
+ render_frame_host->GetFrameTreeNodeId());
+
+ BrowserContextData::StartProxying(profile, std::move(web_contents_getter),
+ std::move(proxied_receiver),
+ std::move(target_factory_remote));
+ return true;
+}
+
+void ProxyingURLLoaderFactory::CreateLoaderAndStart(
+ mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver,
+ int32_t request_id,
+ uint32_t options,
+ const network::ResourceRequest& request,
+ mojo::PendingRemote<network::mojom::URLLoaderClient> client,
+ const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) {
+ requests_.insert(std::make_unique<InProgressRequest>(
+ this, std::move(loader_receiver), request_id, options, request,
+ std::move(client), traffic_annotation));
+}
+
+void ProxyingURLLoaderFactory::Clone(
+ mojo::PendingReceiver<network::mojom::URLLoaderFactory> loader_receiver) {
+ proxy_receivers_.Add(this, std::move(loader_receiver));
+}
+
+void ProxyingURLLoaderFactory::OnTargetFactoryError() {
+ // Stop calls to CreateLoaderAndStart() when |target_factory_| is invalid.
+ target_factory_.reset();
+ proxy_receivers_.Clear();
+
+ MaybeDestroySelf();
+}
+
+void ProxyingURLLoaderFactory::OnProxyBindingError() {
+ if (proxy_receivers_.empty())
+ target_factory_.reset();
+
+ MaybeDestroySelf();
+}
+
+void ProxyingURLLoaderFactory::RemoveRequest(InProgressRequest* request) {
+ auto it = requests_.find(request);
+ DCHECK(it != requests_.end());
+ requests_.erase(it);
+
+ MaybeDestroySelf();
+}
+
+void ProxyingURLLoaderFactory::MaybeDestroySelf() {
+ // Even if all URLLoaderFactory pipes connected to this object have been
+ // closed it has to stay alive until all active requests have completed.
+ if (target_factory_.is_bound() || !requests_.empty())
+ return;
+
+ // Deletes |this|.
+ std::move(on_disconnect_).Run(this);
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h
new file mode 100644
index 00000000000..28589803472
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h
@@ -0,0 +1,100 @@
+// Copyright 2018 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_SIGNIN_CHROME_SIGNIN_PROXYING_URL_LOADER_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_PROXYING_URL_LOADER_FACTORY_H_
+
+#include "base/callback.h"
+#include "base/containers/unique_ptr_adapters.h"
+#include "base/memory/ref_counted_delete_on_sequence.h"
+#include "content/public/browser/web_contents.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/receiver_set.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "services/network/public/mojom/url_loader_factory.mojom.h"
+
+#include <memory>
+#include <set>
+
+namespace content {
+class RenderFrameHost;
+}
+
+namespace signin {
+
+class HeaderModificationDelegate;
+
+// This class is used to modify sub-resource requests made by the renderer
+// that is displaying the GAIA signin realm, to the GAIA signin realm. When
+// such a request is made a proxy is inserted between the renderer and the
+// Network Service to modify request and response headers.
+class ProxyingURLLoaderFactory : public network::mojom::URLLoaderFactory {
+ public:
+ using DisconnectCallback =
+ base::OnceCallback<void(ProxyingURLLoaderFactory*)>;
+
+ // Constructor public for testing purposes. New instances should be created
+ // by calling MaybeProxyRequest().
+ ProxyingURLLoaderFactory(
+ std::unique_ptr<HeaderModificationDelegate> delegate,
+ content::WebContents::Getter web_contents_getter,
+ mojo::PendingReceiver<network::mojom::URLLoaderFactory> receiver,
+ mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory,
+ DisconnectCallback on_disconnect);
+
+ ProxyingURLLoaderFactory(const ProxyingURLLoaderFactory&) = delete;
+ ProxyingURLLoaderFactory& operator=(const ProxyingURLLoaderFactory&) = delete;
+
+ ~ProxyingURLLoaderFactory() override;
+
+ // Called when a renderer needs a URLLoaderFactory to give this module the
+ // opportunity to install a proxy. This is only done when
+ // https://accounts.google.com is loaded in non-incognito mode. Returns true
+ // when |factory_request| has been proxied.
+ static bool MaybeProxyRequest(
+ content::RenderFrameHost* render_frame_host,
+ bool is_navigation,
+ const url::Origin& request_initiator,
+ mojo::PendingReceiver<network::mojom::URLLoaderFactory>*
+ factory_receiver);
+
+ // network::mojom::URLLoaderFactory:
+ void CreateLoaderAndStart(
+ mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver,
+ int32_t request_id,
+ uint32_t options,
+ const network::ResourceRequest& request,
+ mojo::PendingRemote<network::mojom::URLLoaderClient> client,
+ const net::MutableNetworkTrafficAnnotationTag& traffic_annotation)
+ override;
+ void Clone(mojo::PendingReceiver<network::mojom::URLLoaderFactory>
+ loader_receiver) override;
+
+ private:
+ friend class base::DeleteHelper<ProxyingURLLoaderFactory>;
+ friend class base::RefCountedDeleteOnSequence<ProxyingURLLoaderFactory>;
+
+ class InProgressRequest;
+ class ProxyRequestAdapter;
+ class ProxyResponseAdapter;
+
+ void OnTargetFactoryError();
+ void OnProxyBindingError();
+ void RemoveRequest(InProgressRequest* request);
+ void MaybeDestroySelf();
+
+ std::unique_ptr<HeaderModificationDelegate> delegate_;
+ content::WebContents::Getter web_contents_getter_;
+
+ mojo::ReceiverSet<network::mojom::URLLoaderFactory> proxy_receivers_;
+ std::set<std::unique_ptr<InProgressRequest>, base::UniquePtrComparator>
+ requests_;
+ mojo::Remote<network::mojom::URLLoaderFactory> target_factory_;
+ DisconnectCallback on_disconnect_;
+};
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_PROXYING_URL_LOADER_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory_unittest.cc
new file mode 100644
index 00000000000..2d9772cc070
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory_unittest.cc
@@ -0,0 +1,359 @@
+// Copyright 2018 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.
+
+#include "chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h"
+
+#include <algorithm>
+#include <memory>
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/run_loop.h"
+#include "base/test/mock_callback.h"
+#include "chrome/browser/signin/chrome_signin_helper.h"
+#include "chrome/browser/signin/header_modification_delegate.h"
+#include "content/public/test/browser_task_environment.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "services/network/public/cpp/simple_url_loader.h"
+#include "services/network/public/mojom/fetch_api.mojom-shared.h"
+#include "services/network/public/mojom/url_response_head.mojom.h"
+#include "services/network/test/test_url_loader_factory.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using testing::Invoke;
+using testing::_;
+
+namespace signin {
+
+namespace {
+
+class MockDelegate : public HeaderModificationDelegate {
+ public:
+ MockDelegate() = default;
+
+ MockDelegate(const MockDelegate&) = delete;
+ MockDelegate& operator=(const MockDelegate&) = delete;
+
+ ~MockDelegate() override = default;
+
+ MOCK_METHOD1(ShouldInterceptNavigation, bool(content::WebContents* contents));
+ MOCK_METHOD2(ProcessRequest,
+ void(ChromeRequestAdapter* request_adapter,
+ const GURL& redirect_url));
+ MOCK_METHOD2(ProcessResponse,
+ void(ResponseAdapter* response_adapter,
+ const GURL& redirect_url));
+
+ base::WeakPtr<MockDelegate> GetWeakPtr() {
+ return weak_factory_.GetWeakPtr();
+ }
+
+ private:
+ base::WeakPtrFactory<MockDelegate> weak_factory_{this};
+};
+
+content::WebContents::Getter NullWebContentsGetter() {
+ return base::BindRepeating([]() -> content::WebContents* { return nullptr; });
+}
+
+} // namespace
+
+class ChromeSigninProxyingURLLoaderFactoryTest : public testing::Test {
+ public:
+ ChromeSigninProxyingURLLoaderFactoryTest()
+ : test_factory_receiver_(&test_factory_) {}
+
+ ChromeSigninProxyingURLLoaderFactoryTest(
+ const ChromeSigninProxyingURLLoaderFactoryTest&) = delete;
+ ChromeSigninProxyingURLLoaderFactoryTest& operator=(
+ const ChromeSigninProxyingURLLoaderFactoryTest&) = delete;
+
+ ~ChromeSigninProxyingURLLoaderFactoryTest() override {}
+
+ base::WeakPtr<MockDelegate> StartRequest(
+ std::unique_ptr<network::ResourceRequest> request) {
+ loader_ = network::SimpleURLLoader::Create(std::move(request),
+ TRAFFIC_ANNOTATION_FOR_TESTS);
+
+ mojo::Remote<network::mojom::URLLoaderFactory> factory_remote;
+ auto factory_request = factory_remote.BindNewPipeAndPassReceiver();
+ loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
+ factory_remote.get(),
+ base::BindOnce(
+ &ChromeSigninProxyingURLLoaderFactoryTest::OnDownloadComplete,
+ base::Unretained(this)));
+
+ auto delegate = std::make_unique<MockDelegate>();
+ base::WeakPtr<MockDelegate> delegate_weak = delegate->GetWeakPtr();
+
+ proxying_factory_ = std::make_unique<ProxyingURLLoaderFactory>(
+ std::move(delegate), NullWebContentsGetter(),
+ std::move(factory_request),
+ test_factory_receiver_.BindNewPipeAndPassRemote(),
+ base::BindOnce(&ChromeSigninProxyingURLLoaderFactoryTest::OnDisconnect,
+ base::Unretained(this)));
+
+ return delegate_weak;
+ }
+
+ void CloseFactoryReceiver() { test_factory_receiver_.reset(); }
+
+ network::TestURLLoaderFactory* factory() { return &test_factory_; }
+ network::SimpleURLLoader* loader() { return loader_.get(); }
+ std::string* response_body() { return response_body_.get(); }
+
+ void OnDownloadComplete(std::unique_ptr<std::string> body) {
+ response_body_ = std::move(body);
+ }
+
+ private:
+ void OnDisconnect(ProxyingURLLoaderFactory* factory) {
+ EXPECT_EQ(factory, proxying_factory_.get());
+ proxying_factory_.reset();
+ }
+
+ content::BrowserTaskEnvironment task_environment_;
+ std::unique_ptr<network::SimpleURLLoader> loader_;
+ std::unique_ptr<ProxyingURLLoaderFactory> proxying_factory_;
+ network::TestURLLoaderFactory test_factory_;
+ mojo::Receiver<network::mojom::URLLoaderFactory> test_factory_receiver_;
+ std::unique_ptr<std::string> response_body_;
+};
+
+TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, NoModification) {
+ auto request = std::make_unique<network::ResourceRequest>();
+ request->url = GURL("https://google.com/");
+
+ factory()->AddResponse("https://google.com/", "Hello.");
+ base::WeakPtr<MockDelegate> delegate = StartRequest(std::move(request));
+
+ base::RunLoop().RunUntilIdle();
+ EXPECT_EQ(net::OK, loader()->NetError());
+ ASSERT_TRUE(response_body());
+ EXPECT_EQ("Hello.", *response_body());
+}
+
+TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, ModifyHeaders) {
+ const GURL kTestURL("https://google.com/index.html");
+ const GURL kTestReferrer("https://chrome.com/referrer.html");
+ const GURL kTestRedirectURL("https://youtube.com/index.html");
+
+ // Set up the request.
+ auto request = std::make_unique<network::ResourceRequest>();
+ request->url = kTestURL;
+ request->referrer = kTestReferrer;
+ request->destination = network::mojom::RequestDestination::kDocument;
+ request->is_main_frame = true;
+ request->headers.SetHeader("X-Request-1", "Foo");
+
+ base::WeakPtr<MockDelegate> delegate = StartRequest(std::move(request));
+
+ // The first destruction callback added by ProcessRequest is expected to be
+ // called. The second (added after a redirect) will not be.
+ base::MockCallback<base::OnceClosure> destruction_callback;
+ EXPECT_CALL(destruction_callback, Run()).Times(1);
+ base::MockCallback<base::OnceClosure> ignored_destruction_callback;
+ EXPECT_CALL(ignored_destruction_callback, Run()).Times(0);
+
+ // The delegate will be called twice to process a request, first when the
+ // request is started and again when the request is redirected.
+ EXPECT_CALL(*delegate, ProcessRequest(_, _))
+ .WillOnce(
+ Invoke([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_EQ(kTestURL, adapter->GetUrl());
+ EXPECT_EQ(network::mojom::RequestDestination::kDocument,
+ adapter->GetRequestDestination());
+ EXPECT_EQ(GURL("https://chrome.com"), adapter->GetReferrerOrigin());
+
+ EXPECT_TRUE(adapter->HasHeader("X-Request-1"));
+ adapter->RemoveRequestHeaderByName("X-Request-1");
+ EXPECT_FALSE(adapter->HasHeader("X-Request-1"));
+
+ adapter->SetExtraHeaderByName("X-Request-2", "Bar");
+ EXPECT_TRUE(adapter->HasHeader("X-Request-2"));
+
+ EXPECT_EQ(GURL(), redirect_url);
+
+ adapter->SetDestructionCallback(destruction_callback.Get());
+ }))
+ .WillOnce(
+ Invoke([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_EQ(network::mojom::RequestDestination::kDocument,
+ adapter->GetRequestDestination());
+
+ // Changes to the URL and referrer take effect after the redirect
+ // is followed.
+ EXPECT_EQ(kTestURL, adapter->GetUrl());
+ EXPECT_EQ(GURL("https://chrome.com"), adapter->GetReferrerOrigin());
+
+ // X-Request-1 and X-Request-2 were modified in the previous call to
+ // ProcessRequest(). These changes should still be present.
+ EXPECT_FALSE(adapter->HasHeader("X-Request-1"));
+ EXPECT_TRUE(adapter->HasHeader("X-Request-2"));
+
+ adapter->RemoveRequestHeaderByName("X-Request-2");
+ EXPECT_FALSE(adapter->HasHeader("X-Request-2"));
+
+ adapter->SetExtraHeaderByName("X-Request-3", "Baz");
+ EXPECT_TRUE(adapter->HasHeader("X-Request-3"));
+
+ EXPECT_EQ(kTestRedirectURL, redirect_url);
+
+ adapter->SetDestructionCallback(ignored_destruction_callback.Get());
+ }));
+
+ const void* const kResponseUserDataKey = &kResponseUserDataKey;
+ std::unique_ptr<base::SupportsUserData::Data> response_user_data =
+ std::make_unique<base::SupportsUserData::Data>();
+ base::SupportsUserData::Data* response_user_data_ptr =
+ response_user_data.get();
+
+ // The delegate will also be called twice to process a response, first when
+ // the redirect is received and again for the redirect response.
+ EXPECT_CALL(*delegate, ProcessResponse(_, _))
+ .WillOnce(Invoke([&](ResponseAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_EQ(GURL("https://google.com"), adapter->GetOrigin());
+ EXPECT_TRUE(adapter->IsMainFrame());
+
+ adapter->SetUserData(kResponseUserDataKey,
+ std::move(response_user_data));
+ EXPECT_EQ(response_user_data_ptr,
+ adapter->GetUserData(kResponseUserDataKey));
+
+ const net::HttpResponseHeaders* headers = adapter->GetHeaders();
+ EXPECT_TRUE(headers->HasHeader("X-Response-1"));
+ EXPECT_TRUE(headers->HasHeader("X-Response-2"));
+ adapter->RemoveHeader("X-Response-2");
+
+ EXPECT_EQ(kTestRedirectURL, redirect_url);
+ }))
+ .WillOnce(Invoke([&](ResponseAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_EQ(GURL("https://youtube.com"), adapter->GetOrigin());
+ EXPECT_TRUE(adapter->IsMainFrame());
+
+ EXPECT_EQ(response_user_data_ptr,
+ adapter->GetUserData(kResponseUserDataKey));
+
+ const net::HttpResponseHeaders* headers = adapter->GetHeaders();
+ // This is a new response and so previous headers should not carry over.
+ EXPECT_FALSE(headers->HasHeader("X-Response-1"));
+ EXPECT_FALSE(headers->HasHeader("X-Response-2"));
+
+ EXPECT_TRUE(headers->HasHeader("X-Response-3"));
+ EXPECT_TRUE(headers->HasHeader("X-Response-4"));
+ adapter->RemoveHeader("X-Response-3");
+
+ EXPECT_EQ(GURL(), redirect_url);
+ }));
+
+ // Set up a redirect and final response.
+ {
+ net::RedirectInfo redirect_info;
+ redirect_info.new_url = kTestRedirectURL;
+ // An HTTPS to HTTPS redirect such as this wouldn't normally change the
+ // referrer but we do for testing purposes.
+ redirect_info.new_referrer = kTestURL.spec();
+
+ auto redirect_head = network::mojom::URLResponseHead::New();
+ redirect_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
+ redirect_head->headers->SetHeader("X-Response-1", "Foo");
+ redirect_head->headers->SetHeader("X-Response-2", "Bar");
+
+ auto response_head = network::mojom::URLResponseHead::New();
+ response_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
+ response_head->headers->SetHeader("X-Response-3", "Foo");
+ response_head->headers->SetHeader("X-Response-4", "Bar");
+ std::string body("Hello.");
+ network::URLLoaderCompletionStatus status;
+ status.decoded_body_length = body.size();
+
+ network::TestURLLoaderFactory::Redirects redirects;
+ redirects.push_back({redirect_info, std::move(redirect_head)});
+
+ factory()->AddResponse(kTestURL, std::move(response_head), body, status,
+ std::move(redirects));
+ }
+
+ // Wait for the request to complete and check the response.
+ base::RunLoop().RunUntilIdle();
+ EXPECT_EQ(net::OK, loader()->NetError());
+ const network::mojom::URLResponseHead* response_head =
+ loader()->ResponseInfo();
+ ASSERT_TRUE(response_head && response_head->headers);
+ EXPECT_FALSE(response_head->headers->HasHeader("X-Response-3"));
+ EXPECT_TRUE(response_head->headers->HasHeader("X-Response-4"));
+ ASSERT_TRUE(response_body());
+ EXPECT_EQ("Hello.", *response_body());
+
+ // NOTE: TestURLLoaderFactory currently does not expose modifications to
+ // request headers and so we cannot verify that the modifications have been
+ // passed to the target URLLoader.
+}
+
+TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, TargetFactoryFailure) {
+ mojo::Remote<network::mojom::URLLoaderFactory> factory_remote;
+ mojo::PendingRemote<network::mojom::URLLoaderFactory>
+ pending_target_factory_remote;
+ auto target_factory_receiver =
+ pending_target_factory_remote.InitWithNewPipeAndPassReceiver();
+
+ // Without a target factory the proxy will process no requests.
+ auto delegate = std::make_unique<MockDelegate>();
+ EXPECT_CALL(*delegate, ProcessRequest(_, _)).Times(0);
+
+ auto proxying_factory = std::make_unique<ProxyingURLLoaderFactory>(
+ std::move(delegate), NullWebContentsGetter(),
+ factory_remote.BindNewPipeAndPassReceiver(),
+ std::move(pending_target_factory_remote), base::DoNothing());
+
+ // Close |target_factory_receiver| instead of binding it to a
+ // URLLoaderFactory. Spin the message loop so that the connection error
+ // handler can run.
+ target_factory_receiver = mojo::NullReceiver();
+ base::RunLoop().RunUntilIdle();
+
+ auto request = std::make_unique<network::ResourceRequest>();
+ request->url = GURL("https://google.com");
+ auto loader = network::SimpleURLLoader::Create(std::move(request),
+ TRAFFIC_ANNOTATION_FOR_TESTS);
+ loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
+ factory_remote.get(),
+ base::BindOnce(
+ &ChromeSigninProxyingURLLoaderFactoryTest::OnDownloadComplete,
+ base::Unretained(this)));
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_FALSE(response_body());
+ EXPECT_EQ(net::ERR_FAILED, loader->NetError());
+}
+
+TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, RequestKeepAlive) {
+ // Start the request.
+ auto request = std::make_unique<network::ResourceRequest>();
+ request->url = GURL("https://google.com");
+ base::WeakPtr<MockDelegate> delegate = StartRequest(std::move(request));
+ base::RunLoop().RunUntilIdle();
+
+ // Close the factory receiver and spin the message loop again to allow the
+ // connection error handler to be called.
+ CloseFactoryReceiver();
+ base::RunLoop().RunUntilIdle();
+
+ // The ProxyingURLLoaderFactory should not have been destroyed yet because
+ // there is still an in progress request that has not been completed.
+ EXPECT_TRUE(delegate);
+
+ // Complete the request.
+ factory()->AddResponse("https://google.com", "Hello.");
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_FALSE(delegate);
+ EXPECT_EQ(net::OK, loader()->NetError());
+ ASSERT_TRUE(response_body());
+ EXPECT_EQ("Hello.", *response_body());
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.cc b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.cc
new file mode 100644
index 00000000000..63fe0ae0a6b
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.cc
@@ -0,0 +1,141 @@
+// Copyright 2015 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.
+
+#include "chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h"
+
+#include <string>
+#include <vector>
+
+#include "build/build_config.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/signin/core/browser/signin_status_metrics_provider.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+
+#if !defined(OS_ANDROID)
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/browser_list.h"
+#endif
+
+ChromeSigninStatusMetricsProviderDelegate::
+ ChromeSigninStatusMetricsProviderDelegate() {}
+
+ChromeSigninStatusMetricsProviderDelegate::
+ ~ChromeSigninStatusMetricsProviderDelegate() {
+#if !defined(OS_ANDROID)
+ BrowserList::RemoveObserver(this);
+#endif
+
+ auto* factory = IdentityManagerFactory::GetInstance();
+ if (factory)
+ factory->RemoveObserver(this);
+}
+
+void ChromeSigninStatusMetricsProviderDelegate::Initialize() {
+#if !defined(OS_ANDROID)
+ // On Android, there is always only one profile in any situation, opening new
+ // windows (which is possible with only some Android devices) will not change
+ // the opened profiles signin status.
+ BrowserList::AddObserver(this);
+#endif
+
+ auto* factory = IdentityManagerFactory::GetInstance();
+ if (factory)
+ factory->AddObserver(this);
+}
+
+AccountsStatus
+ChromeSigninStatusMetricsProviderDelegate::GetStatusOfAllAccounts() {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ std::vector<Profile*> profile_list = profile_manager->GetLoadedProfiles();
+
+ AccountsStatus accounts_status;
+ accounts_status.num_accounts = profile_list.size();
+ for (Profile* profile : profile_list) {
+#if !defined(OS_ANDROID)
+ if (chrome::GetBrowserCount(profile) == 0) {
+ // The profile is loaded, but there's no opened browser for this profile.
+ continue;
+ }
+#endif
+ accounts_status.num_opened_accounts++;
+
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile->GetOriginalProfile());
+ if (identity_manager &&
+ identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) {
+ accounts_status.num_signed_in_accounts++;
+ }
+ }
+
+ return accounts_status;
+}
+
+std::vector<signin::IdentityManager*>
+ChromeSigninStatusMetricsProviderDelegate::GetIdentityManagersForAllAccounts() {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ std::vector<Profile*> profiles = profile_manager->GetLoadedProfiles();
+
+ std::vector<signin::IdentityManager*> managers;
+ for (Profile* profile : profiles) {
+ auto* identity_manager =
+ IdentityManagerFactory::GetForProfileIfExists(profile);
+ if (identity_manager)
+ managers.push_back(identity_manager);
+ }
+
+ return managers;
+}
+
+#if !defined(OS_ANDROID)
+void ChromeSigninStatusMetricsProviderDelegate::OnBrowserAdded(
+ Browser* browser) {
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(browser->profile());
+
+ // Nothing will change if the opened browser is in incognito mode.
+ if (!identity_manager)
+ return;
+
+ const bool signed_in =
+ identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync);
+ UpdateStatusWhenBrowserAdded(signed_in);
+}
+#endif
+
+void ChromeSigninStatusMetricsProviderDelegate::IdentityManagerCreated(
+ signin::IdentityManager* identity_manager) {
+ owner()->OnIdentityManagerCreated(identity_manager);
+}
+
+void ChromeSigninStatusMetricsProviderDelegate::UpdateStatusWhenBrowserAdded(
+ bool signed_in) {
+#if !defined(OS_ANDROID)
+ SigninStatusMetricsProviderBase::SigninStatus status =
+ owner()->signin_status();
+
+ // NOTE: If |status| is MIXED_SIGNIN_STATUS, this method
+ // intentionally does not update it.
+ if ((status == SigninStatusMetricsProviderBase::ALL_PROFILES_NOT_SIGNED_IN &&
+ signed_in) ||
+ (status == SigninStatusMetricsProviderBase::ALL_PROFILES_SIGNED_IN &&
+ !signed_in)) {
+ owner()->UpdateSigninStatus(
+ SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS);
+ } else if (status == SigninStatusMetricsProviderBase::UNKNOWN_SIGNIN_STATUS) {
+ // If when function ProvideCurrentSessionData() is called, Chrome is
+ // running in the background with no browser window opened, |signin_status_|
+ // will be reset to |UNKNOWN_SIGNIN_STATUS|. Then this newly added browser
+ // is the only opened browser/profile and its signin status represents
+ // the whole status.
+ owner()->UpdateSigninStatus(
+ signed_in
+ ? SigninStatusMetricsProviderBase::ALL_PROFILES_SIGNED_IN
+ : SigninStatusMetricsProviderBase::ALL_PROFILES_NOT_SIGNED_IN);
+ }
+#endif
+}
diff --git a/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h
new file mode 100644
index 00000000000..800d5f8e3f5
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h
@@ -0,0 +1,58 @@
+// Copyright 2015 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_SIGNIN_CHROME_SIGNIN_STATUS_METRICS_PROVIDER_DELEGATE_H_
+#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_STATUS_METRICS_PROVIDER_DELEGATE_H_
+
+#include <vector>
+
+#include "base/gtest_prod_util.h"
+#include "build/build_config.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/signin/core/browser/signin_status_metrics_provider_delegate.h"
+
+#if !defined(OS_ANDROID)
+#include "chrome/browser/ui/browser_list_observer.h"
+#endif // !defined(OS_ANDROID)
+
+class ChromeSigninStatusMetricsProviderDelegate
+ : public SigninStatusMetricsProviderDelegate,
+#if !defined(OS_ANDROID)
+ public BrowserListObserver,
+#endif
+ public IdentityManagerFactory::Observer {
+ public:
+ ChromeSigninStatusMetricsProviderDelegate();
+
+ ChromeSigninStatusMetricsProviderDelegate(
+ const ChromeSigninStatusMetricsProviderDelegate&) = delete;
+ ChromeSigninStatusMetricsProviderDelegate& operator=(
+ const ChromeSigninStatusMetricsProviderDelegate&) = delete;
+
+ ~ChromeSigninStatusMetricsProviderDelegate() override;
+
+ private:
+ FRIEND_TEST_ALL_PREFIXES(ChromeSigninStatusMetricsProviderDelegateTest,
+ UpdateStatusWhenBrowserAdded);
+
+ // SigninStatusMetricsProviderDelegate:
+ void Initialize() override;
+ AccountsStatus GetStatusOfAllAccounts() override;
+ std::vector<signin::IdentityManager*> GetIdentityManagersForAllAccounts()
+ override;
+
+#if !defined(OS_ANDROID)
+ // BrowserListObserver:
+ void OnBrowserAdded(Browser* browser) override;
+#endif
+
+ // IdentityManagerFactoryObserver:
+ void IdentityManagerCreated(
+ signin::IdentityManager* identity_manager) override;
+
+ // Updates the sign-in status right after a new browser is opened.
+ void UpdateStatusWhenBrowserAdded(bool signed_in);
+};
+
+#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_STATUS_METRICS_PROVIDER_DELEGATE_H_
diff --git a/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate_unittest.cc
new file mode 100644
index 00000000000..fd83b3aada5
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate_unittest.cc
@@ -0,0 +1,61 @@
+// Copyright 2014 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.
+
+#include "chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h"
+
+#include <utility>
+
+#include "build/build_config.h"
+#include "components/signin/core/browser/signin_status_metrics_provider.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+#if !defined(OS_ANDROID)
+TEST(ChromeSigninStatusMetricsProviderDelegateTest,
+ UpdateStatusWhenBrowserAdded) {
+ content::BrowserTaskEnvironment task_environment;
+
+ std::unique_ptr<ChromeSigninStatusMetricsProviderDelegate> delegate(
+ new ChromeSigninStatusMetricsProviderDelegate);
+ ChromeSigninStatusMetricsProviderDelegate* raw_delegate = delegate.get();
+ std::unique_ptr<SigninStatusMetricsProvider> metrics_provider =
+ SigninStatusMetricsProvider::CreateInstance(std::move(delegate));
+
+ // Initial status is all signed in and then a signed-in browser is opened.
+ metrics_provider->UpdateInitialSigninStatusForTesting(2, 2);
+ raw_delegate->UpdateStatusWhenBrowserAdded(true);
+ EXPECT_EQ(SigninStatusMetricsProviderBase::ALL_PROFILES_SIGNED_IN,
+ metrics_provider->GetSigninStatusForTesting());
+
+ // Initial status is all signed in and then a signed-out browser is opened.
+ metrics_provider->UpdateInitialSigninStatusForTesting(2, 2);
+ raw_delegate->UpdateStatusWhenBrowserAdded(false);
+ EXPECT_EQ(SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS,
+ metrics_provider->GetSigninStatusForTesting());
+
+ // Initial status is all signed out and then a signed-in browser is opened.
+ metrics_provider->UpdateInitialSigninStatusForTesting(2, 0);
+ raw_delegate->UpdateStatusWhenBrowserAdded(true);
+ EXPECT_EQ(SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS,
+ metrics_provider->GetSigninStatusForTesting());
+
+ // Initial status is all signed out and then a signed-out browser is opened.
+ metrics_provider->UpdateInitialSigninStatusForTesting(2, 0);
+ raw_delegate->UpdateStatusWhenBrowserAdded(false);
+ EXPECT_EQ(SigninStatusMetricsProviderBase::ALL_PROFILES_NOT_SIGNED_IN,
+ metrics_provider->GetSigninStatusForTesting());
+
+ // Initial status is mixed and then a signed-in browser is opened.
+ metrics_provider->UpdateInitialSigninStatusForTesting(2, 1);
+ raw_delegate->UpdateStatusWhenBrowserAdded(true);
+ EXPECT_EQ(SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS,
+ metrics_provider->GetSigninStatusForTesting());
+
+ // Initial status is mixed and then a signed-out browser is opened.
+ metrics_provider->UpdateInitialSigninStatusForTesting(2, 1);
+ raw_delegate->UpdateStatusWhenBrowserAdded(false);
+ EXPECT_EQ(SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS,
+ metrics_provider->GetSigninStatusForTesting());
+}
+#endif
diff --git a/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.cc b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.cc
new file mode 100644
index 00000000000..528fc7b6d44
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.cc
@@ -0,0 +1,189 @@
+// Copyright 2018 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.
+
+#include "chrome/browser/signin/chrome_signin_url_loader_throttle.h"
+
+#include "base/memory/ptr_util.h"
+#include "base/memory/raw_ptr.h"
+#include "chrome/browser/signin/chrome_signin_helper.h"
+#include "chrome/browser/signin/header_modification_delegate.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/mojom/url_response_head.mojom.h"
+
+namespace signin {
+
+class URLLoaderThrottle::ThrottleRequestAdapter : public ChromeRequestAdapter {
+ public:
+ ThrottleRequestAdapter(URLLoaderThrottle* throttle,
+ const net::HttpRequestHeaders& original_headers,
+ net::HttpRequestHeaders* modified_headers,
+ std::vector<std::string>* headers_to_remove)
+ : ChromeRequestAdapter(throttle->request_url_,
+ original_headers,
+ modified_headers,
+ headers_to_remove),
+ throttle_(throttle) {}
+
+ ThrottleRequestAdapter(const ThrottleRequestAdapter&) = delete;
+ ThrottleRequestAdapter& operator=(const ThrottleRequestAdapter&) = delete;
+
+ ~ThrottleRequestAdapter() override = default;
+
+ // ChromeRequestAdapter
+ content::WebContents::Getter GetWebContentsGetter() const override {
+ return throttle_->web_contents_getter_;
+ }
+
+ network::mojom::RequestDestination GetRequestDestination() const override {
+ return throttle_->request_destination_;
+ }
+
+ bool IsFetchLikeAPI() const override {
+ return throttle_->request_is_fetch_like_api_;
+ }
+
+ GURL GetReferrerOrigin() const override {
+ return throttle_->request_referrer_.DeprecatedGetOriginAsURL();
+ }
+
+ void SetDestructionCallback(base::OnceClosure closure) override {
+ if (!throttle_->destruction_callback_)
+ throttle_->destruction_callback_ = std::move(closure);
+ }
+
+ private:
+ const raw_ptr<URLLoaderThrottle> throttle_;
+};
+
+class URLLoaderThrottle::ThrottleResponseAdapter : public ResponseAdapter {
+ public:
+ ThrottleResponseAdapter(URLLoaderThrottle* throttle,
+ net::HttpResponseHeaders* headers)
+ : throttle_(throttle), headers_(headers) {}
+
+ ThrottleResponseAdapter(const ThrottleResponseAdapter&) = delete;
+ ThrottleResponseAdapter& operator=(const ThrottleResponseAdapter&) = delete;
+
+ ~ThrottleResponseAdapter() override = default;
+
+ // ResponseAdapter
+ content::WebContents::Getter GetWebContentsGetter() const override {
+ return throttle_->web_contents_getter_;
+ }
+
+ bool IsMainFrame() const override {
+ return throttle_->request_destination_ ==
+ network::mojom::RequestDestination::kDocument;
+ }
+
+ GURL GetOrigin() const override {
+ return throttle_->request_url_.DeprecatedGetOriginAsURL();
+ }
+
+ const net::HttpResponseHeaders* GetHeaders() const override {
+ return headers_;
+ }
+
+ void RemoveHeader(const std::string& name) override {
+ headers_->RemoveHeader(name);
+ }
+
+ base::SupportsUserData::Data* GetUserData(const void* key) const override {
+ return throttle_->GetUserData(key);
+ }
+
+ void SetUserData(
+ const void* key,
+ std::unique_ptr<base::SupportsUserData::Data> data) override {
+ throttle_->SetUserData(key, std::move(data));
+ }
+
+ private:
+ const raw_ptr<URLLoaderThrottle> throttle_;
+ raw_ptr<net::HttpResponseHeaders> headers_;
+};
+
+// static
+std::unique_ptr<URLLoaderThrottle> URLLoaderThrottle::MaybeCreate(
+ std::unique_ptr<HeaderModificationDelegate> delegate,
+ content::WebContents::Getter web_contents_getter) {
+ if (!delegate->ShouldInterceptNavigation(web_contents_getter.Run()))
+ return nullptr;
+
+ return base::WrapUnique(new URLLoaderThrottle(
+ std::move(delegate), std::move(web_contents_getter)));
+}
+
+URLLoaderThrottle::~URLLoaderThrottle() {
+ if (destruction_callback_)
+ std::move(destruction_callback_).Run();
+}
+
+void URLLoaderThrottle::WillStartRequest(network::ResourceRequest* request,
+ bool* defer) {
+ request_url_ = request->url;
+ request_referrer_ = request->referrer;
+ request_destination_ = request->destination;
+ request_is_fetch_like_api_ = request->is_fetch_like_api;
+
+ net::HttpRequestHeaders modified_request_headers;
+ std::vector<std::string> to_be_removed_request_headers;
+
+ ThrottleRequestAdapter adapter(this, request->headers,
+ &modified_request_headers,
+ &to_be_removed_request_headers);
+ delegate_->ProcessRequest(&adapter, GURL() /* redirect_url */);
+
+ request->headers.MergeFrom(modified_request_headers);
+ for (const std::string& name : to_be_removed_request_headers)
+ request->headers.RemoveHeader(name);
+
+ // We need to keep a full copy of the request headers for later calls to
+ // FixAccountConsistencyRequestHeader. Perhaps this could be replaced with
+ // more specific per-request state.
+ request_headers_.CopyFrom(request->headers);
+ request_cors_exempt_headers_.CopyFrom(request->cors_exempt_headers);
+}
+
+void URLLoaderThrottle::WillRedirectRequest(
+ net::RedirectInfo* redirect_info,
+ const network::mojom::URLResponseHead& response_head,
+ bool* /* defer */,
+ std::vector<std::string>* to_be_removed_request_headers,
+ net::HttpRequestHeaders* modified_request_headers,
+ net::HttpRequestHeaders* modified_cors_exempt_request_headers) {
+ ThrottleRequestAdapter request_adapter(this, request_headers_,
+ modified_request_headers,
+ to_be_removed_request_headers);
+ delegate_->ProcessRequest(&request_adapter, redirect_info->new_url);
+
+ request_headers_.MergeFrom(*modified_request_headers);
+ for (const std::string& name : *to_be_removed_request_headers)
+ request_headers_.RemoveHeader(name);
+
+ // Modifications to |response_head.headers| will be passed to the
+ // URLLoaderClient even though |response_head| is const.
+ ThrottleResponseAdapter response_adapter(this, response_head.headers.get());
+ delegate_->ProcessResponse(&response_adapter, redirect_info->new_url);
+
+ request_url_ = redirect_info->new_url;
+ request_referrer_ = GURL(redirect_info->new_referrer);
+}
+
+void URLLoaderThrottle::WillProcessResponse(
+ const GURL& response_url,
+ network::mojom::URLResponseHead* response_head,
+ bool* defer) {
+ ThrottleResponseAdapter adapter(this, response_head->headers.get());
+ delegate_->ProcessResponse(&adapter, GURL() /* redirect_url */);
+}
+
+URLLoaderThrottle::URLLoaderThrottle(
+ std::unique_ptr<HeaderModificationDelegate> delegate,
+ content::WebContents::Getter web_contents_getter)
+ : delegate_(std::move(delegate)),
+ web_contents_getter_(std::move(web_contents_getter)) {}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.h b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.h
new file mode 100644
index 00000000000..352a84291d7
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.h
@@ -0,0 +1,72 @@
+// Copyright 2018 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_SIGNIN_CHROME_SIGNIN_URL_LOADER_THROTTLE_H_
+#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_URL_LOADER_THROTTLE_H_
+
+#include "base/supports_user_data.h"
+#include "content/public/browser/web_contents.h"
+#include "net/http/http_request_headers.h"
+#include "services/network/public/mojom/fetch_api.mojom-shared.h"
+#include "third_party/blink/public/common/loader/url_loader_throttle.h"
+
+namespace signin {
+
+class HeaderModificationDelegate;
+
+// This class is used to modify the main frame request made when loading the
+// GAIA signin realm.
+class URLLoaderThrottle : public blink::URLLoaderThrottle,
+ public base::SupportsUserData {
+ public:
+ // Creates a new throttle if |delegate| says that this request should be
+ // intercepted.
+ static std::unique_ptr<URLLoaderThrottle> MaybeCreate(
+ std::unique_ptr<HeaderModificationDelegate> delegate,
+ content::WebContents::Getter web_contents_getter);
+
+ URLLoaderThrottle(const URLLoaderThrottle&) = delete;
+ URLLoaderThrottle& operator=(const URLLoaderThrottle&) = delete;
+
+ ~URLLoaderThrottle() override;
+
+ // blink::URLLoaderThrottle
+ void WillStartRequest(network::ResourceRequest* request,
+ bool* defer) override;
+ void WillRedirectRequest(
+ net::RedirectInfo* redirect_info,
+ const network::mojom::URLResponseHead& response_head,
+ bool* defer,
+ std::vector<std::string>* headers_to_remove,
+ net::HttpRequestHeaders* modified_headers,
+ net::HttpRequestHeaders* modified_cors_exempt_headers) override;
+ void WillProcessResponse(const GURL& response_url,
+ network::mojom::URLResponseHead* response_head,
+ bool* defer) override;
+
+ private:
+ class ThrottleRequestAdapter;
+ class ThrottleResponseAdapter;
+
+ URLLoaderThrottle(std::unique_ptr<HeaderModificationDelegate> delegate,
+ content::WebContents::Getter web_contents_getter);
+
+ const std::unique_ptr<HeaderModificationDelegate> delegate_;
+ const content::WebContents::Getter web_contents_getter_;
+
+ // Information about the current request.
+ GURL request_url_;
+ GURL request_referrer_;
+ net::HttpRequestHeaders request_headers_;
+ net::HttpRequestHeaders request_cors_exempt_headers_;
+ network::mojom::RequestDestination request_destination_ =
+ network::mojom::RequestDestination::kEmpty;
+ bool request_is_fetch_like_api_ = false;
+
+ base::OnceClosure destruction_callback_;
+};
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_URL_LOADER_THROTTLE_H_
diff --git a/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle_unittest.cc
new file mode 100644
index 00000000000..f28486ee18a
--- /dev/null
+++ b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle_unittest.cc
@@ -0,0 +1,281 @@
+// Copyright 2018 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.
+
+#include "chrome/browser/signin/chrome_signin_url_loader_throttle.h"
+
+#include "base/bind.h"
+#include "base/memory/ptr_util.h"
+#include "base/test/mock_callback.h"
+#include "chrome/browser/signin/chrome_signin_helper.h"
+#include "chrome/browser/signin/header_modification_delegate.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/mojom/url_response_head.mojom.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using testing::ElementsAre;
+using testing::Invoke;
+using testing::Return;
+using testing::_;
+
+namespace signin {
+
+namespace {
+
+class MockDelegate : public HeaderModificationDelegate {
+ public:
+ MockDelegate() = default;
+
+ MockDelegate(const MockDelegate&) = delete;
+ MockDelegate& operator=(const MockDelegate&) = delete;
+
+ ~MockDelegate() override = default;
+
+ MOCK_METHOD1(ShouldInterceptNavigation, bool(content::WebContents* contents));
+ MOCK_METHOD2(ProcessRequest,
+ void(ChromeRequestAdapter* request_adapter,
+ const GURL& redirect_url));
+ MOCK_METHOD2(ProcessResponse,
+ void(ResponseAdapter* response_adapter,
+ const GURL& redirect_url));
+};
+
+content::WebContents::Getter NullWebContentsGetter() {
+ return base::BindRepeating([]() -> content::WebContents* { return nullptr; });
+}
+
+} // namespace
+
+TEST(ChromeSigninURLLoaderThrottleTest, NoIntercept) {
+ auto* delegate = new MockDelegate();
+
+ EXPECT_CALL(*delegate, ShouldInterceptNavigation(_)).WillOnce(Return(false));
+ EXPECT_FALSE(URLLoaderThrottle::MaybeCreate(base::WrapUnique(delegate),
+ NullWebContentsGetter()));
+}
+
+TEST(ChromeSigninURLLoaderThrottleTest, Intercept) {
+ auto* delegate = new MockDelegate();
+ EXPECT_CALL(*delegate, ShouldInterceptNavigation(_)).WillOnce(Return(true));
+ auto throttle = URLLoaderThrottle::MaybeCreate(base::WrapUnique(delegate),
+ NullWebContentsGetter());
+ ASSERT_TRUE(throttle);
+
+ // Phase 1: Start the request.
+
+ const GURL kTestURL("https://google.com/index.html");
+ const GURL kTestReferrer("https://chrome.com/referrer.html");
+ base::MockCallback<base::OnceClosure> destruction_callback;
+ EXPECT_CALL(*delegate, ProcessRequest(_, _))
+ .WillOnce(
+ Invoke([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_EQ(kTestURL, adapter->GetUrl());
+ EXPECT_EQ(network::mojom::RequestDestination::kDocument,
+ adapter->GetRequestDestination());
+ EXPECT_EQ(GURL("https://chrome.com"), adapter->GetReferrerOrigin());
+
+ EXPECT_TRUE(adapter->HasHeader("X-Request-1"));
+ adapter->RemoveRequestHeaderByName("X-Request-1");
+ EXPECT_FALSE(adapter->HasHeader("X-Request-1"));
+
+ adapter->SetExtraHeaderByName("X-Request-2", "Bar");
+ EXPECT_TRUE(adapter->HasHeader("X-Request-2"));
+
+ EXPECT_EQ(GURL(), redirect_url);
+
+ adapter->SetDestructionCallback(destruction_callback.Get());
+ }));
+
+ network::ResourceRequest request;
+ request.url = kTestURL;
+ request.referrer = kTestReferrer;
+ request.destination = network::mojom::RequestDestination::kDocument;
+ request.headers.SetHeader("X-Request-1", "Foo");
+ bool defer = false;
+ throttle->WillStartRequest(&request, &defer);
+
+ EXPECT_FALSE(request.headers.HasHeader("X-Request-1"));
+ std::string value;
+ EXPECT_TRUE(request.headers.GetHeader("X-Request-2", &value));
+ EXPECT_EQ("Bar", value);
+
+ EXPECT_FALSE(defer);
+
+ testing::Mock::VerifyAndClearExpectations(delegate);
+
+ // Phase 2: Redirect the request.
+
+ const GURL kTestRedirectURL("https://youtube.com/index.html");
+ const void* const kResponseUserDataKey = &kResponseUserDataKey;
+ std::unique_ptr<base::SupportsUserData::Data> response_user_data =
+ std::make_unique<base::SupportsUserData::Data>();
+ base::SupportsUserData::Data* response_user_data_ptr =
+ response_user_data.get();
+
+ EXPECT_CALL(*delegate, ProcessResponse(_, _))
+ .WillOnce(Invoke([&](ResponseAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_EQ(GURL("https://google.com"), adapter->GetOrigin());
+ EXPECT_TRUE(adapter->IsMainFrame());
+
+ adapter->SetUserData(kResponseUserDataKey,
+ std::move(response_user_data));
+ EXPECT_EQ(response_user_data_ptr,
+ adapter->GetUserData(kResponseUserDataKey));
+
+ const net::HttpResponseHeaders* headers = adapter->GetHeaders();
+ EXPECT_TRUE(headers->HasHeader("X-Response-1"));
+ EXPECT_TRUE(headers->HasHeader("X-Response-2"));
+ adapter->RemoveHeader("X-Response-2");
+
+ EXPECT_EQ(kTestRedirectURL, redirect_url);
+ }));
+
+ base::MockCallback<base::OnceClosure> ignored_destruction_callback;
+ EXPECT_CALL(*delegate, ProcessRequest(_, _))
+ .WillOnce(
+ Invoke([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_EQ(network::mojom::RequestDestination::kDocument,
+ adapter->GetRequestDestination());
+
+ // Changes to the URL and referrer take effect after the redirect
+ // is followed.
+ EXPECT_EQ(kTestURL, adapter->GetUrl());
+ EXPECT_EQ(GURL("https://chrome.com"), adapter->GetReferrerOrigin());
+
+ // X-Request-1 and X-Request-2 were modified in the previous call to
+ // ProcessRequest(). These changes should still be present.
+ EXPECT_FALSE(adapter->HasHeader("X-Request-1"));
+ EXPECT_TRUE(adapter->HasHeader("X-Request-2"));
+
+ adapter->RemoveRequestHeaderByName("X-Request-2");
+ EXPECT_FALSE(adapter->HasHeader("X-Request-2"));
+
+ adapter->SetExtraHeaderByName("X-Request-3", "Baz");
+ EXPECT_TRUE(adapter->HasHeader("X-Request-3"));
+
+ EXPECT_EQ(kTestRedirectURL, redirect_url);
+
+ adapter->SetDestructionCallback(ignored_destruction_callback.Get());
+ }));
+
+ net::RedirectInfo redirect_info;
+ redirect_info.new_url = kTestRedirectURL;
+ // An HTTPS to HTTPS redirect such as this wouldn't normally change the
+ // referrer but we do for testing purposes.
+ redirect_info.new_referrer = kTestURL.spec();
+
+ auto response_head = network::mojom::URLResponseHead::New();
+ response_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
+ response_head->headers->SetHeader("X-Response-1", "Foo");
+ response_head->headers->SetHeader("X-Response-2", "Bar");
+
+ std::vector<std::string> request_headers_to_remove;
+ net::HttpRequestHeaders modified_request_headers;
+ net::HttpRequestHeaders modified_cors_exempt_request_headers;
+ throttle->WillRedirectRequest(
+ &redirect_info, *response_head, &defer, &request_headers_to_remove,
+ &modified_request_headers, &modified_cors_exempt_request_headers);
+
+ EXPECT_FALSE(defer);
+
+ EXPECT_TRUE(response_head->headers->HasHeader("X-Response-1"));
+ EXPECT_FALSE(response_head->headers->HasHeader("X-Response-2"));
+
+ EXPECT_THAT(request_headers_to_remove, ElementsAre("X-Request-2"));
+ EXPECT_TRUE(modified_request_headers.GetHeader("X-Request-3", &value));
+ EXPECT_EQ("Baz", value);
+
+ EXPECT_TRUE(modified_cors_exempt_request_headers.IsEmpty());
+
+ testing::Mock::VerifyAndClearExpectations(delegate);
+
+ // Phase 3: Complete the request.
+
+ EXPECT_CALL(*delegate, ProcessResponse(_, _))
+ .WillOnce(Invoke([&](ResponseAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_EQ(GURL("https://youtube.com"), adapter->GetOrigin());
+ EXPECT_TRUE(adapter->IsMainFrame());
+
+ EXPECT_EQ(response_user_data_ptr,
+ adapter->GetUserData(kResponseUserDataKey));
+
+ const net::HttpResponseHeaders* headers = adapter->GetHeaders();
+ // This is a new response and so previous headers should not carry over.
+ EXPECT_FALSE(headers->HasHeader("X-Response-1"));
+ EXPECT_FALSE(headers->HasHeader("X-Response-2"));
+
+ EXPECT_TRUE(headers->HasHeader("X-Response-3"));
+ EXPECT_TRUE(headers->HasHeader("X-Response-4"));
+ adapter->RemoveHeader("X-Response-3");
+
+ EXPECT_EQ(GURL(), redirect_url);
+ }));
+
+ response_head = network::mojom::URLResponseHead::New();
+ response_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
+ response_head->headers->SetHeader("X-Response-3", "Foo");
+ response_head->headers->SetHeader("X-Response-4", "Bar");
+
+ throttle->WillProcessResponse(kTestRedirectURL, response_head.get(), &defer);
+
+ EXPECT_FALSE(response_head->headers->HasHeader("X-Response-3"));
+ EXPECT_TRUE(response_head->headers->HasHeader("X-Response-4"));
+
+ EXPECT_FALSE(defer);
+
+ EXPECT_CALL(destruction_callback, Run()).Times(1);
+ EXPECT_CALL(ignored_destruction_callback, Run()).Times(0);
+ throttle.reset();
+}
+
+TEST(ChromeSigninURLLoaderThrottleTest, InterceptSubFrame) {
+ auto* delegate = new MockDelegate();
+ EXPECT_CALL(*delegate, ShouldInterceptNavigation(_)).WillOnce(Return(true));
+ auto throttle = URLLoaderThrottle::MaybeCreate(base::WrapUnique(delegate),
+ NullWebContentsGetter());
+ ASSERT_TRUE(throttle);
+
+ EXPECT_CALL(*delegate, ProcessRequest(_, _))
+ .Times(2)
+ .WillRepeatedly(
+ [](ChromeRequestAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_EQ(network::mojom::RequestDestination::kIframe,
+ adapter->GetRequestDestination());
+ });
+
+ network::ResourceRequest request;
+ request.url = GURL("https://google.com");
+ request.destination = network::mojom::RequestDestination::kIframe;
+
+ bool defer = false;
+ throttle->WillStartRequest(&request, &defer);
+ EXPECT_FALSE(defer);
+
+ EXPECT_CALL(*delegate, ProcessResponse(_, _))
+ .Times(2)
+ .WillRepeatedly(([](ResponseAdapter* adapter, const GURL& redirect_url) {
+ EXPECT_FALSE(adapter->IsMainFrame());
+ }));
+
+ net::RedirectInfo redirect_info;
+ redirect_info.new_url = GURL("https://youtube.com");
+ auto response_head = network::mojom::URLResponseHead::New();
+
+ std::vector<std::string> request_headers_to_remove;
+ net::HttpRequestHeaders modified_request_headers;
+ net::HttpRequestHeaders modified_cors_exempt_request_headers;
+ throttle->WillRedirectRequest(
+ &redirect_info, *response_head, &defer, &request_headers_to_remove,
+ &modified_request_headers, &modified_cors_exempt_request_headers);
+ EXPECT_FALSE(defer);
+ EXPECT_TRUE(request_headers_to_remove.empty());
+ EXPECT_TRUE(modified_request_headers.IsEmpty());
+ EXPECT_TRUE(modified_cors_exempt_request_headers.IsEmpty());
+
+ throttle->WillProcessResponse(GURL("https://youtube.com"),
+ response_head.get(), &defer);
+ EXPECT_FALSE(defer);
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/chromeos_mirror_account_consistency_browsertest.cc b/chromium/chrome/browser/signin/chromeos_mirror_account_consistency_browsertest.cc
new file mode 100644
index 00000000000..b5b9b6a5379
--- /dev/null
+++ b/chromium/chrome/browser/signin/chromeos_mirror_account_consistency_browsertest.cc
@@ -0,0 +1,174 @@
+// Copyright 2017 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.
+
+#include "chrome/browser/ash/login/login_manager_test.h"
+#include "chrome/browser/ash/login/test/login_manager_mixin.h"
+#include "chrome/browser/ash/profiles/profile_helper.h"
+#include "chrome/browser/prefs/incognito_mode_prefs.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_key.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/supervised_user/supervised_user_constants.h"
+#include "chrome/browser/supervised_user/supervised_user_settings_service.h"
+#include "chrome/browser/supervised_user/supervised_user_settings_service_factory.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/account_id/account_id.h"
+#include "components/google/core/common/google_switches.h"
+#include "components/network_session_configurator/common/network_switches.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "components/user_manager/user.h"
+#include "components/user_manager/user_manager.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "net/test/embedded_test_server/default_handlers.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace {
+
+constexpr char kGaiaDomain[] = "accounts.google.com";
+
+// Checks whether the "X-Chrome-Connected" header of a new request to Google
+// contains |expected_header_value|.
+void TestMirrorRequestForProfile(net::EmbeddedTestServer* test_server,
+ Profile* profile,
+ const std::string& expected_header_value) {
+ GURL gaia_url(test_server->GetURL("/echoheader?X-Chrome-Connected"));
+ GURL::Replacements replace_host;
+ replace_host.SetHostStr(kGaiaDomain);
+ gaia_url = gaia_url.ReplaceComponents(replace_host);
+
+ Browser* browser = Browser::Create(Browser::CreateParams(profile, true));
+ ui_test_utils::NavigateToURLWithDisposition(
+ browser, gaia_url, WindowOpenDisposition::SINGLETON_TAB,
+ ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
+
+ std::string inner_text;
+ ASSERT_TRUE(content::ExecuteScriptAndExtractString(
+ browser->tab_strip_model()->GetActiveWebContents(),
+ "domAutomationController.send(document.body.innerText);", &inner_text));
+ // /echoheader returns "None" if the header isn't set.
+ inner_text = (inner_text == "None") ? "" : inner_text;
+ EXPECT_EQ(expected_header_value, inner_text);
+}
+
+} // namespace
+
+// This is a Chrome OS-only test ensuring that mirror account consistency is
+// enabled for child accounts, but not enabled for other account types.
+class ChromeOsMirrorAccountConsistencyTest : public ash::LoginManagerTest {
+ public:
+ ChromeOsMirrorAccountConsistencyTest(
+ const ChromeOsMirrorAccountConsistencyTest&) = delete;
+ ChromeOsMirrorAccountConsistencyTest& operator=(
+ const ChromeOsMirrorAccountConsistencyTest&) = delete;
+
+ protected:
+ ~ChromeOsMirrorAccountConsistencyTest() override {}
+
+ ChromeOsMirrorAccountConsistencyTest() : LoginManagerTest() {
+ login_mixin_.AppendRegularUsers(1);
+ account_id_ = login_mixin_.users()[0].account_id;
+ }
+
+ void SetUpCommandLine(base::CommandLine* command_line) override {
+ ash::LoginManagerTest::SetUpCommandLine(command_line);
+
+ // HTTPS server only serves a valid cert for localhost, so this is needed to
+ // load pages from "www.google.com" without an interstitial.
+ command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
+
+ // The production code only allows known ports (80 for http and 443 for
+ // https), but the test server runs on a random port.
+ command_line->AppendSwitch(switches::kIgnoreGooglePortNumbers);
+ }
+
+ void SetUpOnMainThread() override {
+ // We can't use BrowserTestBase's EmbeddedTestServer because google.com
+ // URL's have to be https.
+ test_server_ = std::make_unique<net::EmbeddedTestServer>(
+ net::EmbeddedTestServer::TYPE_HTTPS);
+ net::test_server::RegisterDefaultHandlers(test_server_.get());
+ ASSERT_TRUE(test_server_->Start());
+
+ ash::LoginManagerTest::SetUpOnMainThread();
+ }
+
+ AccountId account_id_;
+ ash::LoginManagerMixin login_mixin_{&mixin_host_};
+
+ protected:
+ std::unique_ptr<net::EmbeddedTestServer> test_server_;
+};
+
+// Mirror is enabled for child accounts.
+IN_PROC_BROWSER_TEST_F(ChromeOsMirrorAccountConsistencyTest,
+ TestMirrorRequestChromeOsChildAccount) {
+ // Child user.
+ LoginUser(account_id_);
+
+ user_manager::User* user = user_manager::UserManager::Get()->GetActiveUser();
+ ASSERT_EQ(user, user_manager::UserManager::Get()->GetPrimaryUser());
+ ASSERT_EQ(user, user_manager::UserManager::Get()->FindUser(account_id_));
+ Profile* profile = chromeos::ProfileHelper::Get()->GetProfileByUser(user);
+
+ // Supervised flag uses `FindExtendedAccountInfoForAccountWithRefreshToken`,
+ // so wait for tokens to be loaded.
+ signin::WaitForRefreshTokensLoaded(
+ IdentityManagerFactory::GetForProfile(profile));
+
+ SupervisedUserSettingsService* supervised_user_settings_service =
+ SupervisedUserSettingsServiceFactory::GetForKey(profile->GetProfileKey());
+ supervised_user_settings_service->SetActive(true);
+
+ // Incognito is always disabled for child accounts.
+ PrefService* prefs = profile->GetPrefs();
+ prefs->SetInteger(
+ prefs::kIncognitoModeAvailability,
+ static_cast<int>(IncognitoModePrefs::Availability::kDisabled));
+ ASSERT_EQ(1, signin::PROFILE_MODE_INCOGNITO_DISABLED);
+
+ // TODO(http://crbug.com/1134144): This test seems to test supervised profiles
+ // instead of child accounts. With the current implementation,
+ // X-Chrome-Connected header gets a supervised=true argument only for child
+ // profiles. Verify if these tests needs to be updated to use child accounts
+ // or whether supervised profiles need to be supported as well.
+ TestMirrorRequestForProfile(
+ test_server_.get(), profile,
+ "source=Chrome,mode=1,enable_account_consistency=true,supervised=false,"
+ "consistency_enabled_by_default=false");
+}
+
+// Mirror is enabled for non-child accounts.
+IN_PROC_BROWSER_TEST_F(ChromeOsMirrorAccountConsistencyTest,
+ TestMirrorRequestChromeOsNotChildAccount) {
+ // Not a child user.
+ LoginUser(account_id_);
+
+ user_manager::User* user = user_manager::UserManager::Get()->GetActiveUser();
+ ASSERT_EQ(user, user_manager::UserManager::Get()->GetPrimaryUser());
+ ASSERT_EQ(user, user_manager::UserManager::Get()->FindUser(account_id_));
+ Profile* profile = chromeos::ProfileHelper::Get()->GetProfileByUser(user);
+
+ // Supervised flag uses `FindExtendedAccountInfoForAccountWithRefreshToken`,
+ // so wait for tokens to be loaded.
+ signin::WaitForRefreshTokensLoaded(
+ IdentityManagerFactory::GetForProfile(profile));
+
+ // With Chrome OS Account Manager enabled, this should be true.
+ EXPECT_TRUE(
+ AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile));
+ TestMirrorRequestForProfile(
+ test_server_.get(), profile,
+ "source=Chrome,mode=0,enable_account_consistency=true,supervised=false,"
+ "consistency_enabled_by_default=false");
+}
diff --git a/chromium/chrome/browser/signin/cookie_reminter_factory.cc b/chromium/chrome/browser/signin/cookie_reminter_factory.cc
new file mode 100644
index 00000000000..f53de57d249
--- /dev/null
+++ b/chromium/chrome/browser/signin/cookie_reminter_factory.cc
@@ -0,0 +1,37 @@
+// Copyright 2019 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.
+
+#include "chrome/browser/signin/cookie_reminter_factory.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/signin/core/browser/cookie_reminter.h"
+
+CookieReminterFactory::CookieReminterFactory()
+ : BrowserContextKeyedServiceFactory(
+ "CookieReminter",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(IdentityManagerFactory::GetInstance());
+}
+
+CookieReminterFactory::~CookieReminterFactory() {}
+
+// static
+CookieReminter* CookieReminterFactory::GetForProfile(Profile* profile) {
+ return static_cast<CookieReminter*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+CookieReminterFactory* CookieReminterFactory::GetInstance() {
+ return base::Singleton<CookieReminterFactory>::get();
+}
+
+KeyedService* CookieReminterFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ return new CookieReminter(identity_manager);
+}
diff --git a/chromium/chrome/browser/signin/cookie_reminter_factory.h b/chromium/chrome/browser/signin/cookie_reminter_factory.h
new file mode 100644
index 00000000000..8ce4768bebd
--- /dev/null
+++ b/chromium/chrome/browser/signin/cookie_reminter_factory.h
@@ -0,0 +1,30 @@
+// Copyright 2019 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_SIGNIN_COOKIE_REMINTER_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_COOKIE_REMINTER_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class CookieReminter;
+class Profile;
+
+class CookieReminterFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ static CookieReminter* GetForProfile(Profile* profile);
+ static CookieReminterFactory* GetInstance();
+
+ private:
+ friend struct base::DefaultSingletonTraits<CookieReminterFactory>;
+
+ CookieReminterFactory();
+ ~CookieReminterFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_COOKIE_REMINTER_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/dice_browsertest.cc b/chromium/chrome/browser/signin/dice_browsertest.cc
new file mode 100644
index 00000000000..150647b8b0b
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_browsertest.cc
@@ -0,0 +1,1236 @@
+// Copyright 2017 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.
+
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/auto_reset.h"
+#include "base/base_switches.h"
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/callback_helpers.h"
+#include "base/check.h"
+#include "base/command_line.h"
+#include "base/location.h"
+#include "base/run_loop.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/test/test_mock_time_task_runner.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/time/time.h"
+#include "build/build_config.h"
+#include "chrome/browser/apps/platform_apps/shortcut_manager.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/extensions/api/identity/web_auth_flow.h"
+#include "chrome/browser/policy/cloud/user_policy_signin_service.h"
+#include "chrome/browser/policy/cloud/user_policy_signin_service_internal.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/account_reconcilor_factory.h"
+#include "chrome/browser/signin/chrome_device_id_helper.h"
+#include "chrome/browser/signin/chrome_signin_client.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/chrome_signin_helper.h"
+#include "chrome/browser/signin/dice_response_handler.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_util.h"
+#include "chrome/browser/sync/user_event_service_factory.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/profile_chooser_constants.h"
+#include "chrome/browser/ui/simple_message_box_internal.h"
+#include "chrome/browser/ui/webui/signin/login_ui_service.h"
+#include "chrome/browser/ui/webui/signin/login_ui_service_factory.h"
+#include "chrome/browser/ui/webui/signin/login_ui_test_utils.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/common/url_constants.h"
+#include "chrome/common/webui_url_constants.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/prefs/pref_service.h"
+#include "components/search/ntp_features.h"
+#include "components/signin/core/browser/account_reconcilor.h"
+#include "components/signin/core/browser/dice_header_helper.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "components/signin/public/base/account_consistency_method.h"
+#include "components/signin/public/base/signin_client.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "components/signin/public/identity_manager/primary_account_mutator.h"
+#include "components/sync/base/sync_prefs.h"
+#include "components/sync_user_events/user_event_service.h"
+#include "components/variations/variations_switches.h"
+#include "content/public/browser/browser_task_traits.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/load_notification_details.h"
+#include "content/public/browser/notification_service.h"
+#include "content/public/browser/notification_types.h"
+#include "content/public/common/content_switches.h"
+#include "content/public/test/browser_test.h"
+#include "google_apis/gaia/gaia_switches.h"
+#include "google_apis/gaia/gaia_urls.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/test/embedded_test_server/http_response.h"
+#include "net/test/embedded_test_server/request_handler_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+using net::test_server::BasicHttpResponse;
+using net::test_server::HttpRequest;
+using net::test_server::HttpResponse;
+using signin::AccountConsistencyMethod;
+
+namespace {
+
+constexpr int kAccountReconcilorDelayMs = 10;
+
+enum SignoutType {
+ kSignoutTypeFirst = 0,
+
+ kAllAccounts = 0, // Sign out from all accounts.
+ kMainAccount = 1, // Sign out from main account only.
+ kSecondaryAccount = 2, // Sign out from secondary account only.
+
+ kSignoutTypeLast
+};
+
+const char kAuthorizationCode[] = "authorization_code";
+const char kDiceResponseHeader[] = "X-Chrome-ID-Consistency-Response";
+const char kChromeSyncEndpointURL[] = "/signin/chrome/sync";
+const char kEnableSyncURL[] = "/enable_sync";
+const char kGoogleSignoutResponseHeader[] = "Google-Accounts-SignOut";
+const char kMainGmailEmail[] = "main_email@gmail.com";
+const char kMainManagedEmail[] = "main_email@managed.com";
+const char kNoDiceRequestHeader[] = "NoDiceHeader";
+const char kOAuth2TokenExchangeURL[] = "/oauth2/v4/token";
+const char kOAuth2TokenRevokeURL[] = "/o/oauth2/revoke";
+const char kSecondaryEmail[] = "secondary_email@example.com";
+const char kSigninURL[] = "/signin";
+const char kSigninWithOutageInDiceURL[] = "/signin/outage";
+const char kSignoutURL[] = "/signout";
+
+// Test response that does not complete synchronously. It must be unblocked by
+// calling the completion closure.
+class BlockedHttpResponse : public net::test_server::BasicHttpResponse {
+ public:
+ explicit BlockedHttpResponse(
+ base::OnceCallback<void(base::OnceClosure)> callback)
+ : callback_(std::move(callback)) {}
+
+ void SendResponse(
+ base::WeakPtr<net::test_server::HttpResponseDelegate> delegate) override {
+ // Called on the IO thread to unblock the response.
+ base::OnceClosure unblock_io_thread =
+ base::BindOnce(&BlockedHttpResponse::SendResponseInternal,
+ weak_factory_.GetWeakPtr(), delegate);
+ // Unblock the response from any thread by posting a task to the IO thread.
+ base::OnceClosure unblock_any_thread =
+ base::BindOnce(base::IgnoreResult(&base::TaskRunner::PostTask),
+ base::ThreadTaskRunnerHandle::Get(), FROM_HERE,
+ std::move(unblock_io_thread));
+ // Pass |unblock_any_thread| to the caller on the UI thread.
+ content::GetUIThreadTaskRunner({})->PostTask(
+ FROM_HERE,
+ base::BindOnce(std::move(callback_), std::move(unblock_any_thread)));
+ }
+
+ private:
+ void SendResponseInternal(
+ base::WeakPtr<net::test_server::HttpResponseDelegate> delegate) {
+ if (delegate)
+ BasicHttpResponse::SendResponse(delegate);
+ }
+ base::OnceCallback<void(base::OnceClosure)> callback_;
+
+ base::WeakPtrFactory<BlockedHttpResponse> weak_factory_{this};
+};
+
+} // namespace
+
+namespace FakeGaia {
+
+// Handler for the signin page on the embedded test server.
+// The response has the content of the Dice request header in its body, and has
+// the Dice response header.
+// Handles both the "Chrome Sync" endpoint and the old endpoint.
+std::unique_ptr<HttpResponse> HandleSigninURL(
+ const std::string& main_email,
+ const base::RepeatingCallback<void(const std::string&)>& callback,
+ const HttpRequest& request) {
+ if (!net::test_server::ShouldHandle(request, kSigninURL) &&
+ !net::test_server::ShouldHandle(request, kChromeSyncEndpointURL) &&
+ !net::test_server::ShouldHandle(request, kSigninWithOutageInDiceURL))
+ return nullptr;
+
+ // Extract Dice request header.
+ std::string header_value = kNoDiceRequestHeader;
+ auto it = request.headers.find(signin::kDiceRequestHeader);
+ if (it != request.headers.end())
+ header_value = it->second;
+
+ content::GetUIThreadTaskRunner({})->PostTask(
+ FROM_HERE, base::BindOnce(callback, header_value));
+
+ // Add the SIGNIN dice header.
+ std::unique_ptr<BasicHttpResponse> http_response(new BasicHttpResponse);
+ if (header_value != kNoDiceRequestHeader) {
+ if (net::test_server::ShouldHandle(request, kSigninWithOutageInDiceURL)) {
+ http_response->AddCustomHeader(
+ kDiceResponseHeader,
+ base::StringPrintf("action=SIGNIN,authuser=1,id=%s,email=%s,"
+ "no_authorization_code=true",
+ signin::GetTestGaiaIdForEmail(main_email).c_str(),
+ main_email.c_str()));
+ } else {
+ http_response->AddCustomHeader(
+ kDiceResponseHeader,
+ base::StringPrintf(
+ "action=SIGNIN,authuser=1,id=%s,email=%s,authorization_code=%s",
+ signin::GetTestGaiaIdForEmail(main_email).c_str(),
+ main_email.c_str(), kAuthorizationCode));
+ }
+ }
+
+ // When hitting the Chrome Sync endpoint, redirect to kEnableSyncURL, which
+ // adds the ENABLE_SYNC dice header.
+ if (net::test_server::ShouldHandle(request, kChromeSyncEndpointURL)) {
+ http_response->set_code(net::HTTP_FOUND); // 302 redirect.
+ http_response->AddCustomHeader("location", kEnableSyncURL);
+ }
+
+ http_response->AddCustomHeader("Cache-Control", "no-store");
+ return std::move(http_response);
+}
+
+// Handler for the Gaia endpoint adding the ENABLE_SYNC dice header.
+std::unique_ptr<HttpResponse> HandleEnableSyncURL(
+ const std::string& main_email,
+ const base::RepeatingCallback<void(base::OnceClosure)>& callback,
+ const HttpRequest& request) {
+ if (!net::test_server::ShouldHandle(request, kEnableSyncURL))
+ return nullptr;
+
+ std::unique_ptr<BlockedHttpResponse> http_response =
+ std::make_unique<BlockedHttpResponse>(callback);
+ http_response->AddCustomHeader(
+ kDiceResponseHeader,
+ base::StringPrintf("action=ENABLE_SYNC,authuser=1,id=%s,email=%s",
+ signin::GetTestGaiaIdForEmail(main_email).c_str(),
+ main_email.c_str()));
+ http_response->AddCustomHeader("Cache-Control", "no-store");
+ return std::move(http_response);
+}
+
+// Handler for the signout page on the embedded test server.
+// Responds with a Google-Accounts-SignOut header for the main account, the
+// secondary account, or both (depending on the SignoutType, which is encoded in
+// the query string).
+std::unique_ptr<HttpResponse> HandleSignoutURL(const std::string& main_email,
+ const HttpRequest& request) {
+ if (!net::test_server::ShouldHandle(request, kSignoutURL))
+ return nullptr;
+
+ // Build signout header.
+ int query_value;
+ EXPECT_TRUE(base::StringToInt(request.GetURL().query(), &query_value));
+ SignoutType signout_type = static_cast<SignoutType>(query_value);
+ EXPECT_GE(signout_type, kSignoutTypeFirst);
+ EXPECT_LT(signout_type, kSignoutTypeLast);
+ std::string signout_header_value;
+ if (signout_type == kAllAccounts || signout_type == kMainAccount) {
+ std::string main_gaia_id = signin::GetTestGaiaIdForEmail(main_email);
+ signout_header_value =
+ base::StringPrintf("email=\"%s\", obfuscatedid=\"%s\", sessionindex=1",
+ main_email.c_str(), main_gaia_id.c_str());
+ }
+ if (signout_type == kAllAccounts || signout_type == kSecondaryAccount) {
+ if (!signout_header_value.empty())
+ signout_header_value += ", ";
+ std::string secondary_gaia_id =
+ signin::GetTestGaiaIdForEmail(kSecondaryEmail);
+ signout_header_value +=
+ base::StringPrintf("email=\"%s\", obfuscatedid=\"%s\", sessionindex=2",
+ kSecondaryEmail, secondary_gaia_id.c_str());
+ }
+
+ std::unique_ptr<BasicHttpResponse> http_response(new BasicHttpResponse);
+ http_response->AddCustomHeader(kGoogleSignoutResponseHeader,
+ signout_header_value);
+ http_response->AddCustomHeader("Cache-Control", "no-store");
+ return std::move(http_response);
+}
+
+// Handler for OAuth2 token exchange.
+// Checks that the request is well formatted and returns a refresh token in a
+// JSON dictionary.
+std::unique_ptr<HttpResponse> HandleOAuth2TokenExchangeURL(
+ const base::RepeatingCallback<void(base::OnceClosure)>& callback,
+ const HttpRequest& request) {
+ if (!net::test_server::ShouldHandle(request, kOAuth2TokenExchangeURL))
+ return nullptr;
+
+ // Check that the authorization code is somewhere in the request body.
+ if (!request.has_content)
+ return nullptr;
+ if (request.content.find(kAuthorizationCode) == std::string::npos)
+ return nullptr;
+
+ std::unique_ptr<BlockedHttpResponse> http_response =
+ std::make_unique<BlockedHttpResponse>(callback);
+
+ std::string content =
+ "{"
+ " \"access_token\":\"access_token\","
+ " \"refresh_token\":\"new_refresh_token\","
+ " \"expires_in\":9999"
+ "}";
+
+ http_response->set_content(content);
+ http_response->set_content_type("text/plain");
+ http_response->AddCustomHeader("Cache-Control", "no-store");
+ return std::move(http_response);
+}
+
+// Handler for OAuth2 token revocation.
+std::unique_ptr<HttpResponse> HandleOAuth2TokenRevokeURL(
+ const base::RepeatingClosure& callback,
+ const HttpRequest& request) {
+ if (!net::test_server::ShouldHandle(request, kOAuth2TokenRevokeURL))
+ return nullptr;
+
+ content::GetUIThreadTaskRunner({})->PostTask(FROM_HERE, callback);
+
+ std::unique_ptr<BasicHttpResponse> http_response(new BasicHttpResponse);
+ http_response->AddCustomHeader("Cache-Control", "no-store");
+ return std::move(http_response);
+}
+
+// Handler for ServiceLogin on the embedded test server.
+// Calls the callback with the dice request header, or kNoDiceRequestHeader if
+// there is no Dice header.
+std::unique_ptr<HttpResponse> HandleChromeSigninEmbeddedURL(
+ const base::RepeatingCallback<void(const std::string&)>& callback,
+ const HttpRequest& request) {
+ if (!net::test_server::ShouldHandle(request,
+ "/embedded/setup/chrome/usermenu"))
+ return nullptr;
+
+ std::string dice_request_header(kNoDiceRequestHeader);
+ auto it = request.headers.find(signin::kDiceRequestHeader);
+ if (it != request.headers.end())
+ dice_request_header = it->second;
+ content::GetUIThreadTaskRunner({})->PostTask(
+ FROM_HERE, base::BindOnce(callback, dice_request_header));
+
+ std::unique_ptr<BasicHttpResponse> http_response(new BasicHttpResponse);
+ http_response->AddCustomHeader("Cache-Control", "no-store");
+ return std::move(http_response);
+}
+
+} // namespace FakeGaia
+
+class DiceBrowserTest : public InProcessBrowserTest,
+ public AccountReconcilor::Observer,
+ public signin::IdentityManager::Observer {
+ public:
+ DiceBrowserTest(const DiceBrowserTest&) = delete;
+ DiceBrowserTest& operator=(const DiceBrowserTest&) = delete;
+
+ protected:
+ ~DiceBrowserTest() override {}
+
+ explicit DiceBrowserTest(const std::string& main_email = kMainGmailEmail)
+ : main_email_(main_email),
+ https_server_(net::EmbeddedTestServer::TYPE_HTTPS),
+ enable_sync_requested_(false),
+ token_requested_(false),
+ refresh_token_available_(false),
+ token_revoked_notification_count_(0),
+ token_revoked_count_(0),
+ reconcilor_blocked_count_(0),
+ reconcilor_unblocked_count_(0),
+ reconcilor_started_count_(0) {
+ feature_list_.InitAndEnableFeature(kSupportOAuthOutageInDice);
+ https_server_.RegisterDefaultHandler(base::BindRepeating(
+ &FakeGaia::HandleSigninURL, main_email_,
+ base::BindRepeating(&DiceBrowserTest::OnSigninRequest,
+ base::Unretained(this))));
+ https_server_.RegisterDefaultHandler(base::BindRepeating(
+ &FakeGaia::HandleEnableSyncURL, main_email_,
+ base::BindRepeating(&DiceBrowserTest::OnEnableSyncRequest,
+ base::Unretained(this))));
+ https_server_.RegisterDefaultHandler(
+ base::BindRepeating(&FakeGaia::HandleSignoutURL, main_email_));
+ https_server_.RegisterDefaultHandler(base::BindRepeating(
+ &FakeGaia::HandleOAuth2TokenExchangeURL,
+ base::BindRepeating(&DiceBrowserTest::OnTokenExchangeRequest,
+ base::Unretained(this))));
+ https_server_.RegisterDefaultHandler(base::BindRepeating(
+ &FakeGaia::HandleOAuth2TokenRevokeURL,
+ base::BindRepeating(&DiceBrowserTest::OnTokenRevocationRequest,
+ base::Unretained(this))));
+ https_server_.RegisterDefaultHandler(base::BindRepeating(
+ &FakeGaia::HandleChromeSigninEmbeddedURL,
+ base::BindRepeating(&DiceBrowserTest::OnChromeSigninEmbeddedRequest,
+ base::Unretained(this))));
+ signin::SetDiceAccountReconcilorBlockDelayForTesting(
+ kAccountReconcilorDelayMs);
+ }
+
+ // Navigates to the given path on the test server.
+ void NavigateToURL(const std::string& path) {
+ ASSERT_TRUE(
+ ui_test_utils::NavigateToURL(browser(), https_server_.GetURL(path)));
+ }
+
+ // Returns the identity manager.
+ signin::IdentityManager* GetIdentityManager() {
+ return IdentityManagerFactory::GetForProfile(browser()->profile());
+ }
+
+ // Returns the account ID associated with |main_email_| and its associated
+ // gaia ID.
+ CoreAccountId GetMainAccountID() {
+ return GetIdentityManager()->PickAccountIdForAccount(
+ signin::GetTestGaiaIdForEmail(main_email_), main_email_);
+ }
+
+ // Returns the account ID associated with kSecondaryEmail and its associated
+ // gaia ID.
+ CoreAccountId GetSecondaryAccountID() {
+ return GetIdentityManager()->PickAccountIdForAccount(
+ signin::GetTestGaiaIdForEmail(kSecondaryEmail), kSecondaryEmail);
+ }
+
+ std::string GetDeviceId() {
+ return GetSigninScopedDeviceIdForProfile(browser()->profile());
+ }
+
+ // Signin with a main account and add token for a secondary account.
+ void SetupSignedInAccounts(
+ signin::ConsentLevel primary_account_consent_level) {
+ // Signin main account.
+ AccountInfo primary_account_info = signin::MakePrimaryAccountAvailable(
+ GetIdentityManager(), main_email_, primary_account_consent_level);
+ ASSERT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+ ASSERT_FALSE(
+ GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ GetMainAccountID()));
+ ASSERT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId(
+ primary_account_consent_level));
+
+ // Add a token for a secondary account.
+ AccountInfo secondary_account_info =
+ signin::MakeAccountAvailable(GetIdentityManager(), kSecondaryEmail);
+ ASSERT_TRUE(GetIdentityManager()->HasAccountWithRefreshToken(
+ secondary_account_info.account_id));
+ ASSERT_FALSE(
+ GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ secondary_account_info.account_id));
+ }
+
+ // Navigate to a Gaia URL setting the Google-Accounts-SignOut header.
+ void SignOutWithDice(SignoutType signout_type) {
+ NavigateToURL(base::StringPrintf("%s?%i", kSignoutURL, signout_type));
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(1);
+
+ base::RunLoop().RunUntilIdle();
+ }
+
+ // InProcessBrowserTest:
+ void SetUp() override {
+ ASSERT_TRUE(https_server_.InitializeAndListen());
+ InProcessBrowserTest::SetUp();
+ }
+
+ void SetUpCommandLine(base::CommandLine* command_line) override {
+ const GURL& base_url = https_server_.base_url();
+ command_line->AppendSwitchASCII(switches::kGaiaUrl, base_url.spec());
+ command_line->AppendSwitchASCII(switches::kGoogleApisUrl, base_url.spec());
+ command_line->AppendSwitchASCII(switches::kLsoUrl, base_url.spec());
+ }
+
+ void SetUpOnMainThread() override {
+ InProcessBrowserTest::SetUpOnMainThread();
+ https_server_.StartAcceptingConnections();
+
+ GetIdentityManager()->AddObserver(this);
+ // Wait for the token service to be ready.
+ if (!GetIdentityManager()->AreRefreshTokensLoaded()) {
+ WaitForClosure(&tokens_loaded_quit_closure_);
+ }
+ ASSERT_TRUE(GetIdentityManager()->AreRefreshTokensLoaded());
+
+ AccountReconcilor* reconcilor =
+ AccountReconcilorFactory::GetForProfile(browser()->profile());
+
+ // Reconcilor starts as soon as the token service finishes loading its
+ // credentials. Abort the reconcilor here to make sure tests start in a
+ // stable state.
+ reconcilor->AbortReconcile();
+ reconcilor->SetState(
+ signin_metrics::AccountReconcilorState::ACCOUNT_RECONCILOR_OK);
+ reconcilor->AddObserver(this);
+ }
+
+ void TearDownOnMainThread() override {
+ GetIdentityManager()->RemoveObserver(this);
+ AccountReconcilorFactory::GetForProfile(browser()->profile())
+ ->RemoveObserver(this);
+ }
+
+ // Calls |closure| if it is not null and resets it after.
+ void RunClosureIfValid(base::OnceClosure closure) {
+ if (closure)
+ std::move(closure).Run();
+ }
+
+ // Creates and runs a RunLoop until |closure| is called.
+ void WaitForClosure(base::OnceClosure* closure) {
+ base::RunLoop run_loop;
+ *closure = run_loop.QuitClosure();
+ run_loop.Run();
+ }
+
+ // FakeGaia callbacks:
+ void OnSigninRequest(const std::string& dice_request_header) {
+ EXPECT_EQ(dice_request_header != kNoDiceRequestHeader,
+ IsReconcilorBlocked());
+ dice_request_header_ = dice_request_header;
+ RunClosureIfValid(std::move(signin_requested_quit_closure_));
+ }
+
+ void OnChromeSigninEmbeddedRequest(const std::string& dice_request_header) {
+ dice_request_header_ = dice_request_header;
+ RunClosureIfValid(std::move(chrome_signin_embedded_quit_closure_));
+ }
+
+ void OnEnableSyncRequest(base::OnceClosure unblock_response_closure) {
+ EXPECT_TRUE(IsReconcilorBlocked());
+ enable_sync_requested_ = true;
+ RunClosureIfValid(std::move(enable_sync_requested_quit_closure_));
+ unblock_enable_sync_response_closure_ = std::move(unblock_response_closure);
+ }
+
+ void OnTokenExchangeRequest(base::OnceClosure unblock_response_closure) {
+ // The token must be exchanged only once.
+ EXPECT_FALSE(token_requested_);
+ EXPECT_TRUE(IsReconcilorBlocked());
+ token_requested_ = true;
+ RunClosureIfValid(std::move(token_requested_quit_closure_));
+ unblock_token_exchange_response_closure_ =
+ std::move(unblock_response_closure);
+ }
+
+ void OnTokenRevocationRequest() {
+ ++token_revoked_count_;
+ RunClosureIfValid(std::move(token_revoked_quit_closure_));
+ }
+
+ // AccountReconcilor::Observer:
+ void OnBlockReconcile() override { ++reconcilor_blocked_count_; }
+ void OnUnblockReconcile() override {
+ ++reconcilor_unblocked_count_;
+ RunClosureIfValid(std::move(unblock_count_quit_closure_));
+ }
+ void OnStateChanged(signin_metrics::AccountReconcilorState state) override {
+ if (state ==
+ signin_metrics::AccountReconcilorState::ACCOUNT_RECONCILOR_RUNNING) {
+ ++reconcilor_started_count_;
+ }
+ }
+
+ // signin::IdentityManager::Observer
+ void OnPrimaryAccountChanged(
+ const signin::PrimaryAccountChangeEvent& event) override {
+ if (event.GetEventTypeFor(signin::ConsentLevel::kSync) ==
+ signin::PrimaryAccountChangeEvent::Type::kSet) {
+ RunClosureIfValid(std::move(on_primary_account_set_quit_closure_));
+ }
+ }
+
+ void OnRefreshTokenUpdatedForAccount(
+ const CoreAccountInfo& account_info) override {
+ if (account_info.account_id == GetMainAccountID()) {
+ refresh_token_available_ = true;
+ RunClosureIfValid(std::move(refresh_token_available_quit_closure_));
+ }
+ }
+
+ void OnRefreshTokenRemovedForAccount(
+ const CoreAccountId& account_id) override {
+ ++token_revoked_notification_count_;
+ }
+
+ void OnRefreshTokensLoaded() override {
+ RunClosureIfValid(std::move(tokens_loaded_quit_closure_));
+ }
+
+ // Returns true if the account reconcilor is currently blocked.
+ bool IsReconcilorBlocked() {
+ EXPECT_GE(reconcilor_blocked_count_, reconcilor_unblocked_count_);
+ EXPECT_LE(reconcilor_blocked_count_, reconcilor_unblocked_count_ + 1);
+ return (reconcilor_unblocked_count_ + 1) == reconcilor_blocked_count_;
+ }
+
+ // Waits until |reconcilor_unblocked_count_| reaches |count|.
+ void WaitForReconcilorUnblockedCount(int count) {
+ if (reconcilor_unblocked_count_ == count)
+ return;
+
+ ASSERT_EQ(count - 1, reconcilor_unblocked_count_);
+ // Wait for the timeout after the request is complete.
+ WaitForClosure(&unblock_count_quit_closure_);
+ EXPECT_EQ(count, reconcilor_unblocked_count_);
+ }
+
+ // Waits until the user consented for sync.
+ void WaitForSigninSucceeded() {
+ if (GetIdentityManager()
+ ->GetPrimaryAccountId(signin::ConsentLevel::kSync)
+ .empty()) {
+ WaitForClosure(&on_primary_account_set_quit_closure_);
+ }
+ }
+
+ // Waits for the ENABLE_SYNC request to hit the server, and unblocks the
+ // response. If this is not called, ENABLE_SYNC will not be sent by the
+ // server.
+ // Note: this does not wait for the response to reach Chrome.
+ void SendEnableSyncResponse() {
+ if (!enable_sync_requested_)
+ WaitForClosure(&enable_sync_requested_quit_closure_);
+ DCHECK(unblock_enable_sync_response_closure_);
+ std::move(unblock_enable_sync_response_closure_).Run();
+ }
+
+ // Waits until the token request is sent to the server, the response is
+ // received and the refresh token is available. If this is not called, the
+ // refresh token will not be sent by the server.
+ void SendRefreshTokenResponse() {
+ // Wait for the request hitting the server.
+ if (!token_requested_)
+ WaitForClosure(&token_requested_quit_closure_);
+ EXPECT_TRUE(token_requested_);
+ // Unblock the server response.
+ DCHECK(unblock_token_exchange_response_closure_);
+ std::move(unblock_token_exchange_response_closure_).Run();
+ // Wait for the response coming back.
+ if (!refresh_token_available_)
+ WaitForClosure(&refresh_token_available_quit_closure_);
+ EXPECT_TRUE(refresh_token_available_);
+ }
+
+ void WaitForTokenRevokedCount(int count) {
+ EXPECT_LE(token_revoked_count_, count);
+ while (token_revoked_count_ < count)
+ WaitForClosure(&token_revoked_quit_closure_);
+ EXPECT_EQ(count, token_revoked_count_);
+ }
+
+ DiceResponseHandler* GetDiceResponseHandler() {
+ return DiceResponseHandler::GetForProfile(browser()->profile());
+ }
+
+ const std::string main_email_;
+ net::EmbeddedTestServer https_server_;
+ bool enable_sync_requested_;
+ bool token_requested_;
+ bool refresh_token_available_;
+ int token_revoked_notification_count_;
+ int token_revoked_count_;
+ int reconcilor_blocked_count_;
+ int reconcilor_unblocked_count_;
+ int reconcilor_started_count_;
+ std::string dice_request_header_;
+ base::test::ScopedFeatureList feature_list_;
+
+ // Unblocks the server responses.
+ base::OnceClosure unblock_token_exchange_response_closure_;
+ base::OnceClosure unblock_enable_sync_response_closure_;
+
+ // Used for waiting asynchronous events.
+ base::OnceClosure enable_sync_requested_quit_closure_;
+ base::OnceClosure token_requested_quit_closure_;
+ base::OnceClosure token_revoked_quit_closure_;
+ base::OnceClosure refresh_token_available_quit_closure_;
+ base::OnceClosure chrome_signin_embedded_quit_closure_;
+ base::OnceClosure unblock_count_quit_closure_;
+ base::OnceClosure tokens_loaded_quit_closure_;
+ base::OnceClosure on_primary_account_set_quit_closure_;
+ base::OnceClosure signin_requested_quit_closure_;
+};
+
+// Checks that signin on Gaia triggers the fetch for a refresh token.
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, Signin) {
+ EXPECT_EQ(0, reconcilor_started_count_);
+
+ // Navigate to Gaia and sign in.
+ NavigateToURL(kSigninURL);
+
+ // Check that the Dice request header was sent.
+ std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id();
+ EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s,"
+ "signin_mode=all_accounts,"
+ "signout_mode=show_confirmation",
+ signin::kDiceProtocolVersion, client_id.c_str(),
+ GetDeviceId().c_str()),
+ dice_request_header_);
+
+ // Check that the token was requested and added to the token service.
+ SendRefreshTokenResponse();
+ EXPECT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+ // Sync should not be enabled.
+ EXPECT_TRUE(GetIdentityManager()
+ ->GetPrimaryAccountId(signin::ConsentLevel::kSync)
+ .empty());
+
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(1);
+ EXPECT_EQ(1, reconcilor_started_count_);
+}
+
+// Checks that the account reconcilor is blocked when where was OAuth
+// outage in Dice, and unblocked after the timeout.
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, SupportOAuthOutageInDice) {
+ DiceResponseHandler* dice_response_handler = GetDiceResponseHandler();
+ scoped_refptr<base::TestMockTimeTaskRunner> task_runner =
+ new base::TestMockTimeTaskRunner();
+ dice_response_handler->SetTaskRunner(task_runner);
+ NavigateToURL(kSigninWithOutageInDiceURL);
+ // Check that the Dice request header was sent.
+ std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id();
+ EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s,"
+ "signin_mode=all_accounts,"
+ "signout_mode=show_confirmation",
+ signin::kDiceProtocolVersion, client_id.c_str(),
+ GetDeviceId().c_str()),
+ dice_request_header_);
+ // Check that the reconcilor was blocked and not unblocked before timeout.
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ task_runner->FastForwardBy(
+ base::Hours(kLockAccountReconcilorTimeoutHours / 2));
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ task_runner->FastForwardBy(
+ base::Hours((kLockAccountReconcilorTimeoutHours + 1) / 2));
+ // Wait until reconcilor is unblocked.
+ WaitForReconcilorUnblockedCount(1);
+}
+
+// Checks that re-auth on Gaia triggers the fetch for a refresh token.
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, Reauth) {
+ EXPECT_EQ(0, reconcilor_started_count_);
+
+ // Start from a signed-in state.
+ SetupSignedInAccounts(signin::ConsentLevel::kSync);
+ EXPECT_EQ(1, reconcilor_started_count_);
+
+ // Navigate to Gaia and sign in again with the main account.
+ NavigateToURL(kSigninURL);
+
+ // Check that the Dice request header was sent.
+ std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id();
+ EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s,"
+ "signin_mode=all_accounts,"
+ "signout_mode=show_confirmation",
+ signin::kDiceProtocolVersion, client_id.c_str(),
+ GetDeviceId().c_str()),
+ dice_request_header_);
+
+ // Check that the token was requested and added to the token service.
+ SendRefreshTokenResponse();
+ EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSync));
+
+ // Old token must not be revoked (see http://crbug.com/865189).
+ EXPECT_EQ(0, token_revoked_notification_count_);
+
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(1);
+ EXPECT_EQ(2, reconcilor_started_count_);
+}
+
+// Checks that the Dice signout flow works and deletes all tokens.
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, SignoutMainAccount) {
+ // Start from a signed-in state.
+ SetupSignedInAccounts(signin::ConsentLevel::kSync);
+
+ // Signout from main account.
+ SignOutWithDice(kMainAccount);
+
+ // Check that the user is in error state.
+ EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSync));
+ EXPECT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+ EXPECT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ GetMainAccountID()));
+ EXPECT_TRUE(GetIdentityManager()->HasAccountWithRefreshToken(
+ GetSecondaryAccountID()));
+
+ // Token for main account is revoked on server but not notified in the client.
+ EXPECT_EQ(0, token_revoked_notification_count_);
+ WaitForTokenRevokedCount(1);
+
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(1);
+}
+
+// Checks that signing out from a secondary account does not delete the main
+// token.
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, SignoutSecondaryAccount) {
+ // Start from a signed-in state.
+ SetupSignedInAccounts(signin::ConsentLevel::kSync);
+
+ // Signout from secondary account.
+ SignOutWithDice(kSecondaryAccount);
+
+ // Check that the user is still signed in from main account, but secondary
+ // token is deleted.
+ EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSync));
+ EXPECT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+ EXPECT_FALSE(GetIdentityManager()->HasAccountWithRefreshToken(
+ GetSecondaryAccountID()));
+ EXPECT_EQ(1, token_revoked_notification_count_);
+ WaitForTokenRevokedCount(1);
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(1);
+}
+
+// Checks that the Dice signout flow works and deletes all tokens.
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, SignoutAllAccounts) {
+ // Start from a signed-in state.
+ SetupSignedInAccounts(signin::ConsentLevel::kSync);
+
+ // Signout from all accounts.
+ SignOutWithDice(kAllAccounts);
+
+ // Check that the user is in error state.
+ EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSync));
+ EXPECT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+ EXPECT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ GetMainAccountID()));
+ EXPECT_FALSE(GetIdentityManager()->HasAccountWithRefreshToken(
+ GetSecondaryAccountID()));
+
+ // Token for main account is revoked on server but not notified in the client.
+ EXPECT_EQ(1, token_revoked_notification_count_);
+ WaitForTokenRevokedCount(2);
+
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(1);
+}
+
+// Checks that Dice request header is not set from request from WebUI.
+// See https://crbug.com/428396
+#if defined(OS_WIN)
+#define MAYBE_NoDiceFromWebUI DISABLED_NoDiceFromWebUI
+#else
+#define MAYBE_NoDiceFromWebUI NoDiceFromWebUI
+#endif
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, MAYBE_NoDiceFromWebUI) {
+ // Navigate to Gaia and from the native tab, which uses an extension.
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(
+ browser(), GURL("chrome:chrome-signin?reason=5")));
+
+ // Check that the request had no Dice request header.
+ if (dice_request_header_.empty())
+ WaitForClosure(&chrome_signin_embedded_quit_closure_);
+ EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_);
+ EXPECT_EQ(0, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(0);
+}
+
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest,
+ NoDiceExtensionConsent_LaunchWebAuthFlow) {
+ auto web_auth_flow = std::make_unique<extensions::WebAuthFlow>(
+ nullptr, browser()->profile(), https_server_.GetURL(kSigninURL),
+ extensions::WebAuthFlow::INTERACTIVE,
+ extensions::WebAuthFlow::LAUNCH_WEB_AUTH_FLOW);
+ web_auth_flow->Start();
+
+ if (dice_request_header_.empty())
+ WaitForClosure(&signin_requested_quit_closure_);
+
+ EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_);
+ EXPECT_EQ(0, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(0);
+
+ // Delete the web auth flow (uses DeleteSoon).
+ web_auth_flow.release()->DetachDelegateAndDelete();
+ base::RunLoop().RunUntilIdle();
+}
+
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, DiceExtensionConsent_GetAuthToken) {
+ // Signin from extension consent flow.
+ class DummyDelegate : public extensions::WebAuthFlow::Delegate {
+ public:
+ void OnAuthFlowFailure(extensions::WebAuthFlow::Failure failure) override {}
+ ~DummyDelegate() override = default;
+ };
+
+ DummyDelegate delegate;
+ auto web_auth_flow = std::make_unique<extensions::WebAuthFlow>(
+ &delegate, browser()->profile(), https_server_.GetURL(kSigninURL),
+ extensions::WebAuthFlow::INTERACTIVE,
+ extensions::WebAuthFlow::GET_AUTH_TOKEN);
+ web_auth_flow->Start();
+
+ // Check that the token was requested and added to the token service.
+ SendRefreshTokenResponse();
+ EXPECT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+
+ // Check that the Dice request header was sent.
+ std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id();
+ EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s,"
+ "signin_mode=all_accounts,"
+ "signout_mode=show_confirmation",
+ signin::kDiceProtocolVersion, client_id.c_str(),
+ GetDeviceId().c_str()),
+ dice_request_header_);
+
+ // Sync should not be enabled.
+ EXPECT_TRUE(GetIdentityManager()
+ ->GetPrimaryAccountId(signin::ConsentLevel::kSync)
+ .empty());
+
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(1);
+ EXPECT_EQ(1, reconcilor_started_count_);
+
+ // Delete the web auth flow (uses DeleteSoon).
+ web_auth_flow.release()->DetachDelegateAndDelete();
+ base::RunLoop().RunUntilIdle();
+}
+
+// Tests that Sync is enabled if the ENABLE_SYNC response is received after the
+// refresh token.
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, EnableSyncAfterToken) {
+ EXPECT_EQ(0, reconcilor_started_count_);
+
+ // Signin using the Chrome Sync endpoint.
+ browser()->signin_view_controller()->ShowSignin(
+ profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN,
+ signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS);
+
+ // Receive token.
+ EXPECT_FALSE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+ SendRefreshTokenResponse();
+ EXPECT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+
+ // Receive ENABLE_SYNC.
+ SendEnableSyncResponse();
+
+ // Check that the Dice request header was sent, with signout confirmation.
+ std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id();
+ EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s,"
+ "signin_mode=all_accounts,"
+ "signout_mode=show_confirmation",
+ signin::kDiceProtocolVersion, client_id.c_str(),
+ GetDeviceId().c_str()),
+ dice_request_header_);
+
+ content::WindowedNotificationObserver ntp_url_observer(
+ content::NOTIFICATION_LOAD_STOP,
+ base::BindRepeating([](const content::NotificationSource&,
+ const content::NotificationDetails& details) {
+ auto url =
+ content::Details<content::LoadNotificationDetails>(details)->url;
+ // Some test flags (e.g. ForceWebRequestProxyForTest) can change whether
+ // the reported NTP URL is chrome://newtab or chrome://new-tab-page.
+ return url == GURL(chrome::kChromeUINewTabPageURL) ||
+ url == GURL(chrome::kChromeUINewTabURL);
+ }));
+
+ WaitForSigninSucceeded();
+ EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSync));
+
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(1);
+ EXPECT_EQ(1, reconcilor_started_count_);
+
+ // Check that the tab was navigated to the NTP.
+ ntp_url_observer.Wait();
+
+ // Dismiss the Sync confirmation UI.
+ EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog(browser()));
+}
+
+// Tests that Sync is enabled if the ENABLE_SYNC response is received before the
+// refresh token.
+
+// https://crbug.com/1082858
+#if (defined(OS_LINUX) || defined(OS_CHROMEOS)) && !defined(NDEBUG)
+#define MAYBE_EnableSyncBeforeToken DISABLED_EnableSyncBeforeToken
+#else
+#define MAYBE_EnableSyncBeforeToken EnableSyncBeforeToken
+#endif
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, MAYBE_EnableSyncBeforeToken) {
+ EXPECT_EQ(0, reconcilor_started_count_);
+
+ ui_test_utils::UrlLoadObserver enable_sync_url_observer(
+ https_server_.GetURL(kEnableSyncURL),
+ content::NotificationService::AllSources());
+
+ // Signin using the Chrome Sync endpoint.
+ browser()->signin_view_controller()->ShowSignin(
+ profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN,
+ signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS);
+
+ // Receive ENABLE_SYNC.
+ SendEnableSyncResponse();
+ // Wait for the page to be fully loaded.
+ enable_sync_url_observer.Wait();
+
+ // Receive token.
+ EXPECT_FALSE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+ SendRefreshTokenResponse();
+ EXPECT_TRUE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+
+ // Check that the Dice request header was sent, with signout confirmation.
+ std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id();
+ EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s,"
+ "signin_mode=all_accounts,"
+ "signout_mode=show_confirmation",
+ signin::kDiceProtocolVersion, client_id.c_str(),
+ GetDeviceId().c_str()),
+ dice_request_header_);
+
+ ui_test_utils::UrlLoadObserver ntp_url_observer(
+ GURL(chrome::kChromeUINewTabURL),
+ content::NotificationService::AllSources());
+
+ WaitForSigninSucceeded();
+ EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSync));
+
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(1);
+ EXPECT_EQ(1, reconcilor_started_count_);
+
+ // Check that the tab was navigated to the NTP.
+ ntp_url_observer.Wait();
+
+ // Dismiss the Sync confirmation UI.
+ EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog(browser()));
+}
+
+// Tests that turning off Dice via preferences works when singed out.
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, PRE_TurnOffDice_SignedOut) {
+ ASSERT_FALSE(
+ GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
+ ASSERT_TRUE(AccountConsistencyModeManager::IsDiceEnabledForProfile(
+ browser()->profile()));
+
+ // Turn off Dice for this profile.
+ browser()->profile()->GetPrefs()->SetBoolean(
+ prefs::kSigninAllowedOnNextStartup, false);
+}
+
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, TurnOffDice_SignedOut) {
+ // Check that Dice is disabled.
+ EXPECT_FALSE(
+ browser()->profile()->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+ EXPECT_FALSE(browser()->profile()->GetPrefs()->GetBoolean(
+ prefs::kSigninAllowedOnNextStartup));
+ EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile(
+ browser()->profile()));
+
+ EXPECT_FALSE(
+ GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
+
+ // Navigate to Gaia and sign in.
+ NavigateToURL(kSigninURL);
+ // Check that the Dice request header was not sent.
+ EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_);
+ EXPECT_EQ(0, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(0);
+}
+
+// Tests that turning off Dice via preferences works when signed in without sync
+// consent.
+//
+// Regression test for crbug/1254325
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, PRE_TurnOffDice_NotOptedIntoSync) {
+ SetupSignedInAccounts(signin::ConsentLevel::kSignin);
+
+ ASSERT_TRUE(
+ GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
+ ASSERT_FALSE(
+ GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ ASSERT_TRUE(AccountConsistencyModeManager::IsDiceEnabledForProfile(
+ browser()->profile()));
+
+ // Turn off Dice for this profile.
+ browser()->profile()->GetPrefs()->SetBoolean(
+ prefs::kSigninAllowedOnNextStartup, false);
+}
+
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, TurnOffDice_NotOptedIntoSync) {
+ // Check that Dice is disabled.
+ EXPECT_FALSE(
+ browser()->profile()->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+ EXPECT_FALSE(browser()->profile()->GetPrefs()->GetBoolean(
+ prefs::kSigninAllowedOnNextStartup));
+ EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile(
+ browser()->profile()));
+
+ EXPECT_FALSE(
+ GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
+ EXPECT_FALSE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+ EXPECT_TRUE(GetIdentityManager()->GetAccountsWithRefreshTokens().empty());
+
+ // Navigate to Gaia and sign in.
+ NavigateToURL(kSigninURL);
+ // Check that the Dice request header was not sent.
+ EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_);
+ EXPECT_EQ(0, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(0);
+}
+
+// Tests that turning off Dice via preferences works when signed in with sync
+// consent
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, PRE_TurnOffDice_OptedIntoSync) {
+ // Sign the profile in and turn sync on.
+ SetupSignedInAccounts(signin::ConsentLevel::kSync);
+ syncer::SyncPrefs(browser()->profile()->GetPrefs()).SetFirstSetupComplete();
+
+ ASSERT_TRUE(
+ GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ ASSERT_TRUE(AccountConsistencyModeManager::IsDiceEnabledForProfile(
+ browser()->profile()));
+
+ // Turn off Dice for this profile.
+ browser()->profile()->GetPrefs()->SetBoolean(
+ prefs::kSigninAllowedOnNextStartup, false);
+}
+
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, TurnOffDice_OptedIntoSync) {
+ EXPECT_FALSE(
+ browser()->profile()->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+ EXPECT_FALSE(browser()->profile()->GetPrefs()->GetBoolean(
+ prefs::kSigninAllowedOnNextStartup));
+ EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile(
+ browser()->profile()));
+
+ EXPECT_FALSE(
+ GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ EXPECT_FALSE(
+ GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
+ EXPECT_FALSE(
+ GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID()));
+ EXPECT_TRUE(GetIdentityManager()->GetAccountsWithRefreshTokens().empty());
+
+ // Navigate to Gaia and sign in.
+ NavigateToURL(kSigninURL);
+ // Check that the Dice request header was not sent.
+ EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_);
+ EXPECT_EQ(0, reconcilor_blocked_count_);
+ WaitForReconcilorUnblockedCount(0);
+}
+
+// Checks that Dice is disabled in incognito mode.
+IN_PROC_BROWSER_TEST_F(DiceBrowserTest, Incognito) {
+ Browser* incognito_browser = Browser::Create(Browser::CreateParams(
+ browser()->profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true),
+ true));
+
+ // Check that Dice is disabled.
+ EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile(
+ incognito_browser->profile()));
+}
+
+// This test is not specifically related to DICE, but it extends
+// |DiceBrowserTest| for convenience.
+class DiceManageAccountBrowserTest : public DiceBrowserTest {
+ public:
+ DiceManageAccountBrowserTest()
+ : DiceBrowserTest(kMainManagedEmail),
+ // Skip showing the error message box to avoid freezing the main thread.
+ skip_message_box_auto_reset_(
+ &chrome::internal::g_should_skip_message_box_for_test,
+ true),
+ // Force the policy component to prohibit clearing the primary account
+ // even when the policy core component is not initialized.
+ prohibit_sigout_auto_reset_(
+ &policy::internal::g_force_prohibit_signout_for_tests,
+ true) {}
+
+ void SetUp() override {
+#if defined(OS_WIN)
+ // Shortcut deletion delays tests shutdown on Win-7 and results in time out.
+ // See crbug.com/1073451.
+ AppShortcutManager::SuppressShortcutsForTesting();
+#endif
+ DiceBrowserTest::SetUp();
+ }
+
+ protected:
+ base::AutoReset<bool> skip_message_box_auto_reset_;
+ base::AutoReset<bool> prohibit_sigout_auto_reset_;
+ unsigned int number_of_profiles_added_ = 0;
+};
+
+// Tests that prohiting sign-in on startup for a managed profile clears the
+// profile directory on next start-up.
+IN_PROC_BROWSER_TEST_F(DiceManageAccountBrowserTest,
+ PRE_ClearManagedProfileOnStartup) {
+ // Ensure that there are not deleted profiles before running this test.
+ PrefService* local_state = g_browser_process->local_state();
+ DCHECK(local_state);
+ const base::ListValue* deleted_profiles =
+ local_state->GetList(prefs::kProfilesDeleted);
+ ASSERT_TRUE(deleted_profiles);
+ ASSERT_TRUE(deleted_profiles->GetList().empty());
+
+ // Sign the profile in.
+ SetupSignedInAccounts(signin::ConsentLevel::kSync);
+
+ // Prohibit sign-in on next start-up.
+ browser()->profile()->GetPrefs()->SetBoolean(
+ prefs::kSigninAllowedOnNextStartup, false);
+}
+
+IN_PROC_BROWSER_TEST_F(DiceManageAccountBrowserTest,
+ ClearManagedProfileOnStartup) {
+ // Initial profile should have been deleted as sign-in and sign out were no
+ // longer allowed.
+ PrefService* local_state = g_browser_process->local_state();
+ DCHECK(local_state);
+ const base::ListValue* deleted_profiles =
+ local_state->GetList(prefs::kProfilesDeleted);
+ EXPECT_TRUE(deleted_profiles);
+ EXPECT_EQ(1U, deleted_profiles->GetList().size());
+
+ content::RunAllTasksUntilIdle();
+
+ // Verify that there is an active profile.
+ Profile* initial_profile = browser()->profile();
+ EXPECT_EQ(1U, g_browser_process->profile_manager()->GetNumberOfProfiles());
+ EXPECT_EQ(g_browser_process->profile_manager()->GetLastUsedProfile(),
+ initial_profile);
+}
diff --git a/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.cc b/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.cc
new file mode 100644
index 00000000000..ecd8893f92b
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.cc
@@ -0,0 +1,179 @@
+// Copyright 2020 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.
+
+#include "chrome/browser/signin/dice_intercepted_session_startup_helper.h"
+
+#include <algorithm>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/time/time.h"
+#include "chrome/browser/signin/account_reconcilor_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/ui/browser_navigator.h"
+#include "chrome/browser/ui/browser_navigator_params.h"
+#include "chrome/common/webui_url_constants.h"
+#include "components/signin/public/base/multilogin_parameters.h"
+#include "components/signin/public/identity_manager/accounts_cookie_mutator.h"
+#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h"
+#include "components/signin/public/identity_manager/set_accounts_in_cookie_result.h"
+#include "content/public/browser/web_contents.h"
+#include "google_apis/gaia/gaia_auth_fetcher.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+#include "google_apis/gaia/google_service_auth_error.h"
+#include "url/gurl.h"
+
+namespace {
+
+// Returns true if |account_id| is signed in the cookies.
+bool CookieInfoContains(const signin::AccountsInCookieJarInfo& cookie_info,
+ const CoreAccountId& account_id) {
+ const std::vector<gaia::ListedAccount>& accounts =
+ cookie_info.signed_in_accounts;
+ return std::find_if(accounts.begin(), accounts.end(),
+ [&account_id](const gaia::ListedAccount& account) {
+ return account.id == account_id;
+ }) != accounts.end();
+}
+
+} // namespace
+
+DiceInterceptedSessionStartupHelper::DiceInterceptedSessionStartupHelper(
+ Profile* profile,
+ bool is_new_profile,
+ CoreAccountId account_id,
+ content::WebContents* tab_to_move)
+ : profile_(profile),
+ use_multilogin_(is_new_profile),
+ account_id_(account_id) {
+ if (tab_to_move)
+ web_contents_ = tab_to_move->GetWeakPtr();
+}
+
+DiceInterceptedSessionStartupHelper::~DiceInterceptedSessionStartupHelper() =
+ default;
+
+void DiceInterceptedSessionStartupHelper::Startup(base::OnceClosure callback) {
+ callback_ = std::move(callback);
+
+ // Wait until the account is set in cookies of the newly created profile
+ // before opening the URL, so that the user is signed-in in content area. If
+ // the account is still not in the cookie after some timeout, proceed without
+ // cookies, so that the user can at least take some action in the new profile.
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile_);
+ signin::AccountsInCookieJarInfo cookie_info =
+ identity_manager->GetAccountsInCookieJar();
+ if (cookie_info.accounts_are_fresh &&
+ CookieInfoContains(cookie_info, account_id_)) {
+ MoveTab();
+ } else {
+ // Set the timeout.
+ on_cookie_update_timeout_.Reset(base::BindOnce(
+ &DiceInterceptedSessionStartupHelper::MoveTab, base::Unretained(this)));
+ // Adding accounts to the cookies can be an expensive operation. In
+ // particular the ExternalCCResult fetch may time out after multiple seconds
+ // (see kExternalCCResultTimeoutSeconds and https://crbug.com/750316#c37).
+ base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
+ FROM_HERE, on_cookie_update_timeout_.callback(), base::Seconds(12));
+
+ accounts_in_cookie_observer_.Observe(identity_manager);
+ if (use_multilogin_)
+ StartupMultilogin(identity_manager);
+ else
+ StartupReconcilor(identity_manager);
+ }
+}
+
+void DiceInterceptedSessionStartupHelper::OnAccountsInCookieUpdated(
+ const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info,
+ const GoogleServiceAuthError& error) {
+ if (error != GoogleServiceAuthError::AuthErrorNone())
+ return;
+ if (!accounts_in_cookie_jar_info.accounts_are_fresh)
+ return;
+ if (!CookieInfoContains(accounts_in_cookie_jar_info, account_id_))
+ return;
+
+ MoveTab();
+}
+
+void DiceInterceptedSessionStartupHelper::OnStateChanged(
+ signin_metrics::AccountReconcilorState state) {
+ DCHECK(!use_multilogin_);
+ if (state == signin_metrics::ACCOUNT_RECONCILOR_ERROR) {
+ reconcile_error_encountered_ = true;
+ return;
+ }
+
+ // TODO(https://crbug.com/1051864): remove this when the cookie updates are
+ // correctly sent after reconciliation.
+ if (state == signin_metrics::ACCOUNT_RECONCILOR_OK) {
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile_);
+ // GetAccountsInCookieJar() automatically re-schedules a /ListAccounts call
+ // if the cookie is not fresh.
+ signin::AccountsInCookieJarInfo cookie_info =
+ identity_manager->GetAccountsInCookieJar();
+ OnAccountsInCookieUpdated(cookie_info,
+ GoogleServiceAuthError::AuthErrorNone());
+ }
+}
+
+void DiceInterceptedSessionStartupHelper::StartupMultilogin(
+ signin::IdentityManager* identity_manager) {
+ // Lock the reconcilor to avoid making multiple multilogin calls.
+ reconcilor_lock_ = std::make_unique<AccountReconcilor::Lock>(
+ AccountReconcilorFactory::GetForProfile(profile_));
+
+ // Start the multilogin call.
+ signin::MultiloginParameters params = {
+ /*mode=*/gaia::MultiloginMode::MULTILOGIN_UPDATE_COOKIE_ACCOUNTS_ORDER,
+ /*accounts_to_send=*/{account_id_}};
+ identity_manager->GetAccountsCookieMutator()->SetAccountsInCookie(
+ params, gaia::GaiaSource::kChrome,
+ base::BindOnce(
+ &DiceInterceptedSessionStartupHelper::OnSetAccountInCookieCompleted,
+ weak_factory_.GetWeakPtr()));
+}
+
+void DiceInterceptedSessionStartupHelper::StartupReconcilor(
+ signin::IdentityManager* identity_manager) {
+ // TODO(https://crbug.com/1051864): cookie notifications are not triggered
+ // when the account is added by the reconcilor. Observe the reconcilor and
+ // re-trigger the cookie update when it completes.
+ reconcilor_observer_.Observe(
+ AccountReconcilorFactory::GetForProfile(profile_));
+ identity_manager->GetAccountsCookieMutator()->TriggerCookieJarUpdate();
+}
+
+void DiceInterceptedSessionStartupHelper::OnSetAccountInCookieCompleted(
+ signin::SetAccountsInCookieResult result) {
+ DCHECK(use_multilogin_);
+ MoveTab();
+}
+
+void DiceInterceptedSessionStartupHelper::MoveTab() {
+ accounts_in_cookie_observer_.Reset();
+ reconcilor_observer_.Reset();
+ on_cookie_update_timeout_.Cancel();
+ reconcilor_lock_.reset();
+
+ GURL url_to_open = GURL(chrome::kChromeUINewTabURL);
+ // If the intercepted web contents is still alive, close it now.
+ if (web_contents_) {
+ url_to_open = web_contents_->GetURL();
+ web_contents_->Close();
+ }
+
+ // Open a new browser.
+ NavigateParams params(profile_, url_to_open,
+ ui::PAGE_TRANSITION_AUTO_BOOKMARK);
+ Navigate(&params);
+
+ if (callback_)
+ std::move(callback_).Run();
+}
diff --git a/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.h b/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.h
new file mode 100644
index 00000000000..2895a90d66b
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.h
@@ -0,0 +1,102 @@
+// Copyright 2020 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_SIGNIN_DICE_INTERCEPTED_SESSION_STARTUP_HELPER_H_
+#define CHROME_BROWSER_SIGNIN_DICE_INTERCEPTED_SESSION_STARTUP_HELPER_H_
+
+#include "base/callback_forward.h"
+#include "base/cancelable_callback.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/scoped_observation.h"
+#include "components/signin/core/browser/account_reconcilor.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "content/public/browser/web_contents_observer.h"
+#include "google_apis/gaia/core_account_id.h"
+
+namespace content {
+class WebContents;
+}
+
+namespace signin {
+struct AccountsInCookieJarInfo;
+class IdentityManager;
+enum class SetAccountsInCookieResult;
+}
+
+class GoogleServiceAuthError;
+class Profile;
+
+// Called when the user accepted the dice signin interception and the new
+// profile has been created. Creates a new browser and moves the intercepted tab
+// to the new browser.
+// It is assumed that the account is already in the profile, but not necessarily
+// in the content area (cookies).
+class DiceInterceptedSessionStartupHelper
+ : public signin::IdentityManager::Observer,
+ public AccountReconcilor::Observer {
+ public:
+ // |profile| is the new profile that was created after signin interception.
+ // |account_id| is the main account for the profile, it's already in the
+ // profile.
+ // |tab_to_move| is the tab where the interception happened, in the source
+ // profile.
+ DiceInterceptedSessionStartupHelper(Profile* profile,
+ bool is_new_profile,
+ CoreAccountId account_id,
+ content::WebContents* tab_to_move);
+
+ ~DiceInterceptedSessionStartupHelper() override;
+
+ DiceInterceptedSessionStartupHelper(
+ const DiceInterceptedSessionStartupHelper&) = delete;
+ DiceInterceptedSessionStartupHelper& operator=(
+ const DiceInterceptedSessionStartupHelper&) = delete;
+
+ // Start up the session. Can only be called once.
+ void Startup(base::OnceClosure callback);
+
+ // signin::IdentityManager::Observer:
+ void OnAccountsInCookieUpdated(
+ const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info,
+ const GoogleServiceAuthError& error) override;
+
+ // AccountReconcilor::Observer:
+ void OnStateChanged(signin_metrics::AccountReconcilorState state) override;
+
+ private:
+ // For new profiles, the account is added directly using multilogin.
+ void StartupMultilogin(signin::IdentityManager* identity_manager);
+
+ // For existing profiles, simply wait for the reconcilor to update the
+ // accounts.
+ void StartupReconcilor(signin::IdentityManager* identity_manager);
+
+ // Called when multilogin completes.
+ void OnSetAccountInCookieCompleted(signin::SetAccountsInCookieResult result);
+
+ // Creates a browser with a new tab, and closes the intercepted tab if it's
+ // still open.
+ void MoveTab();
+
+ const raw_ptr<Profile> profile_;
+ base::WeakPtr<content::WebContents> web_contents_;
+ bool use_multilogin_;
+ CoreAccountId account_id_;
+ base::OnceClosure callback_;
+ bool reconcile_error_encountered_ = false;
+ base::ScopedObservation<signin::IdentityManager,
+ signin::IdentityManager::Observer>
+ accounts_in_cookie_observer_{this};
+ base::ScopedObservation<AccountReconcilor, AccountReconcilor::Observer>
+ reconcilor_observer_{this};
+ std::unique_ptr<AccountReconcilor::Lock> reconcilor_lock_;
+ // Timeout while waiting for the account to be added to the cookies in the new
+ // profile.
+ base::CancelableOnceCallback<void()> on_cookie_update_timeout_;
+
+ base::WeakPtrFactory<DiceInterceptedSessionStartupHelper> weak_factory_{this};
+};
+
+#endif // CHROME_BROWSER_SIGNIN_DICE_INTERCEPTED_SESSION_STARTUP_HELPER_H_
diff --git a/chromium/chrome/browser/signin/dice_response_handler.cc b/chromium/chrome/browser/signin/dice_response_handler.cc
new file mode 100644
index 00000000000..88d7563846f
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_response_handler.cc
@@ -0,0 +1,426 @@
+// Copyright 2017 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.
+
+#include "chrome/browser/signin/dice_response_handler.h"
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/location.h"
+#include "base/logging.h"
+#include "base/memory/singleton.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/strings/stringprintf.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/time/time.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profiles_state.h"
+#include "chrome/browser/signin/about_signin_internals_factory.h"
+#include "chrome/browser/signin/account_reconcilor_factory.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/ui/webui/profile_helper.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+#include "components/signin/core/browser/about_signin_internals.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "components/signin/public/base/signin_client.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "components/signin/public/identity_manager/accounts_mutator.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "google_apis/gaia/gaia_auth_fetcher.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+#include "google_apis/gaia/google_service_auth_error.h"
+
+const int kDiceTokenFetchTimeoutSeconds = 10;
+// Timeout for locking the account reconcilor when
+// there was OAuth outage in Dice.
+const int kLockAccountReconcilorTimeoutHours = 12;
+
+const base::Feature kSupportOAuthOutageInDice{"SupportOAuthOutageInDice",
+ base::FEATURE_ENABLED_BY_DEFAULT};
+
+namespace {
+
+// The UMA histograms that logs events related to Dice responses.
+const char kDiceResponseHeaderHistogram[] = "Signin.DiceResponseHeader";
+const char kDiceTokenFetchResultHistogram[] = "Signin.DiceTokenFetchResult";
+
+// Used for UMA. Do not reorder, append new values at the end.
+enum DiceResponseHeader {
+ // Received a signin header.
+ kSignin = 0,
+ // Received a signout header including the Chrome primary account.
+ kSignoutPrimary = 1,
+ // Received a signout header for other account(s).
+ kSignoutSecondary = 2,
+ // Received a "EnableSync" header.
+ kEnableSync = 3,
+
+ kDiceResponseHeaderCount
+};
+
+// Used for UMA. Do not reorder, append new values at the end.
+enum DiceTokenFetchResult {
+ // The token fetch succeeded.
+ kFetchSuccess = 0,
+ // The token fetch was aborted. For example, if another request for the same
+ // account is already in flight.
+ kFetchAbort = 1,
+ // The token fetch failed because Gaia responsed with an error.
+ kFetchFailure = 2,
+ // The token fetch failed because no response was received from Gaia.
+ kFetchTimeout = 3,
+
+ kDiceTokenFetchResultCount
+};
+
+class DiceResponseHandlerFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ // Returns an instance of the factory singleton.
+ static DiceResponseHandlerFactory* GetInstance() {
+ return base::Singleton<DiceResponseHandlerFactory>::get();
+ }
+
+ static DiceResponseHandler* GetForProfile(Profile* profile) {
+ return static_cast<DiceResponseHandler*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+ }
+
+ private:
+ friend struct base::DefaultSingletonTraits<DiceResponseHandlerFactory>;
+
+ DiceResponseHandlerFactory()
+ : BrowserContextKeyedServiceFactory(
+ "DiceResponseHandler",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(AboutSigninInternalsFactory::GetInstance());
+ DependsOn(AccountReconcilorFactory::GetInstance());
+ DependsOn(ChromeSigninClientFactory::GetInstance());
+ DependsOn(IdentityManagerFactory::GetInstance());
+ }
+
+ ~DiceResponseHandlerFactory() override {}
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* context) const override {
+ if (context->IsOffTheRecord())
+ return nullptr;
+
+ Profile* profile = static_cast<Profile*>(context);
+ return new DiceResponseHandler(
+ ChromeSigninClientFactory::GetForProfile(profile),
+ IdentityManagerFactory::GetForProfile(profile),
+ AccountReconcilorFactory::GetForProfile(profile),
+ AboutSigninInternalsFactory::GetForProfile(profile),
+ profile->GetPath());
+ }
+};
+
+// Histogram macros expand to a lot of code, so it is better to wrap them in
+// functions.
+
+void RecordDiceResponseHeader(DiceResponseHeader header) {
+ UMA_HISTOGRAM_ENUMERATION(kDiceResponseHeaderHistogram, header,
+ kDiceResponseHeaderCount);
+}
+
+void RecordDiceFetchTokenResult(DiceTokenFetchResult result) {
+ UMA_HISTOGRAM_ENUMERATION(kDiceTokenFetchResultHistogram, result,
+ kDiceTokenFetchResultCount);
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+// DiceTokenFetcher
+////////////////////////////////////////////////////////////////////////////////
+
+DiceResponseHandler::DiceTokenFetcher::DiceTokenFetcher(
+ const std::string& gaia_id,
+ const std::string& email,
+ const std::string& authorization_code,
+ SigninClient* signin_client,
+ AccountReconcilor* account_reconcilor,
+ std::unique_ptr<ProcessDiceHeaderDelegate> delegate,
+ DiceResponseHandler* dice_response_handler)
+ : gaia_id_(gaia_id),
+ email_(email),
+ authorization_code_(authorization_code),
+ delegate_(std::move(delegate)),
+ dice_response_handler_(dice_response_handler),
+ timeout_closure_(
+ base::BindOnce(&DiceResponseHandler::DiceTokenFetcher::OnTimeout,
+ base::Unretained(this))),
+ should_enable_sync_(false) {
+ DCHECK(dice_response_handler_);
+ account_reconcilor_lock_ =
+ std::make_unique<AccountReconcilor::Lock>(account_reconcilor);
+ gaia_auth_fetcher_ =
+ signin_client->CreateGaiaAuthFetcher(this, gaia::GaiaSource::kChrome);
+ VLOG(1) << "Start fetching token for account: " << email;
+ gaia_auth_fetcher_->StartAuthCodeForOAuth2TokenExchange(authorization_code_);
+ base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
+ FROM_HERE, timeout_closure_.callback(),
+ base::Seconds(kDiceTokenFetchTimeoutSeconds));
+}
+
+DiceResponseHandler::DiceTokenFetcher::~DiceTokenFetcher() {}
+
+void DiceResponseHandler::DiceTokenFetcher::OnTimeout() {
+ RecordDiceFetchTokenResult(kFetchTimeout);
+ gaia_auth_fetcher_.reset();
+ timeout_closure_.Cancel();
+ dice_response_handler_->OnTokenExchangeFailure(
+ this, GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED));
+ // |this| may be deleted at this point.
+}
+
+void DiceResponseHandler::DiceTokenFetcher::OnClientOAuthSuccess(
+ const GaiaAuthConsumer::ClientOAuthResult& result) {
+ RecordDiceFetchTokenResult(kFetchSuccess);
+ gaia_auth_fetcher_.reset();
+ timeout_closure_.Cancel();
+ dice_response_handler_->OnTokenExchangeSuccess(
+ this, result.refresh_token, result.is_under_advanced_protection);
+ // |this| may be deleted at this point.
+}
+
+void DiceResponseHandler::DiceTokenFetcher::OnClientOAuthFailure(
+ const GoogleServiceAuthError& error) {
+ RecordDiceFetchTokenResult(kFetchFailure);
+ gaia_auth_fetcher_.reset();
+ timeout_closure_.Cancel();
+ dice_response_handler_->OnTokenExchangeFailure(this, error);
+ // |this| may be deleted at this point.
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// DiceResponseHandler
+////////////////////////////////////////////////////////////////////////////////
+
+// static
+DiceResponseHandler* DiceResponseHandler::GetForProfile(Profile* profile) {
+ return DiceResponseHandlerFactory::GetForProfile(profile);
+}
+
+DiceResponseHandler::DiceResponseHandler(
+ SigninClient* signin_client,
+ signin::IdentityManager* identity_manager,
+ AccountReconcilor* account_reconcilor,
+ AboutSigninInternals* about_signin_internals,
+ const base::FilePath& profile_path)
+ : signin_client_(signin_client),
+ identity_manager_(identity_manager),
+ account_reconcilor_(account_reconcilor),
+ about_signin_internals_(about_signin_internals),
+ profile_path_(profile_path) {
+ DCHECK(signin_client_);
+ DCHECK(identity_manager_);
+ DCHECK(account_reconcilor_);
+ DCHECK(about_signin_internals_);
+}
+
+DiceResponseHandler::~DiceResponseHandler() {}
+
+void DiceResponseHandler::ProcessDiceHeader(
+ const signin::DiceResponseParams& dice_params,
+ std::unique_ptr<ProcessDiceHeaderDelegate> delegate) {
+ DCHECK(delegate);
+ switch (dice_params.user_intention) {
+ case signin::DiceAction::SIGNIN: {
+ const signin::DiceResponseParams::AccountInfo& info =
+ dice_params.signin_info->account_info;
+ ProcessDiceSigninHeader(
+ info.gaia_id, info.email, dice_params.signin_info->authorization_code,
+ dice_params.signin_info->no_authorization_code, std::move(delegate));
+ return;
+ }
+ case signin::DiceAction::ENABLE_SYNC: {
+ const signin::DiceResponseParams::AccountInfo& info =
+ dice_params.enable_sync_info->account_info;
+ ProcessEnableSyncHeader(info.gaia_id, info.email, std::move(delegate));
+ return;
+ }
+ case signin::DiceAction::SIGNOUT:
+ DCHECK_GT(dice_params.signout_info->account_infos.size(), 0u);
+ ProcessDiceSignoutHeader(dice_params.signout_info->account_infos);
+ return;
+ case signin::DiceAction::NONE:
+ NOTREACHED() << "Invalid Dice response parameters.";
+ return;
+ }
+ NOTREACHED();
+}
+
+size_t DiceResponseHandler::GetPendingDiceTokenFetchersCountForTesting() const {
+ return token_fetchers_.size();
+}
+
+void DiceResponseHandler::OnTimeoutUnlockReconcilor() {
+ lock_.reset();
+}
+
+void DiceResponseHandler::SetTaskRunner(
+ scoped_refptr<base::SequencedTaskRunner> task_runner) {
+ task_runner_ = std::move(task_runner);
+}
+
+void DiceResponseHandler::ProcessDiceSigninHeader(
+ const std::string& gaia_id,
+ const std::string& email,
+ const std::string& authorization_code,
+ bool no_authorization_code,
+ std::unique_ptr<ProcessDiceHeaderDelegate> delegate) {
+ if (no_authorization_code) {
+ if (base::FeatureList::IsEnabled(kSupportOAuthOutageInDice)) {
+ lock_ = std::make_unique<AccountReconcilor::Lock>(account_reconcilor_);
+ about_signin_internals_->OnRefreshTokenReceived(
+ "Missing authorization code due to OAuth outage in Dice.");
+ if (!timer_) {
+ timer_ = std::make_unique<base::OneShotTimer>();
+ if (task_runner_)
+ timer_->SetTaskRunner(task_runner_);
+ }
+ // If there is already another lock, the timer will be reset and
+ // we'll wait another full timeout.
+ timer_->Start(
+ FROM_HERE, base::Hours(kLockAccountReconcilorTimeoutHours),
+ base::BindOnce(&DiceResponseHandler::OnTimeoutUnlockReconcilor,
+ base::Unretained(this)));
+ }
+ return;
+ }
+
+ DCHECK(!gaia_id.empty());
+ DCHECK(!email.empty());
+ DCHECK(!authorization_code.empty());
+ VLOG(1) << "Start processing Dice signin response";
+ RecordDiceResponseHeader(kSignin);
+
+ for (auto it = token_fetchers_.begin(); it != token_fetchers_.end(); ++it) {
+ if ((it->get()->gaia_id() == gaia_id) && (it->get()->email() == email) &&
+ (it->get()->authorization_code() == authorization_code)) {
+ RecordDiceFetchTokenResult(kFetchAbort);
+ return; // There is already a request in flight with the same parameters.
+ }
+ }
+ token_fetchers_.push_back(std::make_unique<DiceTokenFetcher>(
+ gaia_id, email, authorization_code, signin_client_, account_reconcilor_,
+ std::move(delegate), this));
+}
+
+void DiceResponseHandler::ProcessEnableSyncHeader(
+ const std::string& gaia_id,
+ const std::string& email,
+ std::unique_ptr<ProcessDiceHeaderDelegate> delegate) {
+ VLOG(1) << "Start processing Dice enable sync response";
+ RecordDiceResponseHeader(kEnableSync);
+ for (auto it = token_fetchers_.begin(); it != token_fetchers_.end(); ++it) {
+ DiceTokenFetcher* fetcher = it->get();
+ if (fetcher->gaia_id() == gaia_id) {
+ DCHECK(gaia::AreEmailsSame(fetcher->email(), email));
+ // If there is a fetch in progress for a resfresh token for the given
+ // account, then simply mark it to enable sync after the refresh token is
+ // available.
+ fetcher->set_should_enable_sync(true);
+ return; // There is already a request in flight with the same parameters.
+ }
+ }
+ CoreAccountId account_id =
+ identity_manager_->PickAccountIdForAccount(gaia_id, email);
+ delegate->EnableSync(account_id);
+}
+
+void DiceResponseHandler::ProcessDiceSignoutHeader(
+ const std::vector<signin::DiceResponseParams::AccountInfo>& account_infos) {
+ VLOG(1) << "Start processing Dice signout response";
+
+ CoreAccountId primary_account =
+ identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSync);
+ bool primary_account_signed_out = false;
+ auto* accounts_mutator = identity_manager_->GetAccountsMutator();
+ for (const auto& account_info : account_infos) {
+ CoreAccountId signed_out_account =
+ identity_manager_->PickAccountIdForAccount(account_info.gaia_id,
+ account_info.email);
+ if (signed_out_account == primary_account) {
+ primary_account_signed_out = true;
+ RecordDiceResponseHeader(kSignoutPrimary);
+
+ // Put the account in error state.
+ accounts_mutator->InvalidateRefreshTokenForPrimaryAccount(
+ signin_metrics::SourceForRefreshTokenOperation::
+ kDiceResponseHandler_Signout);
+ } else {
+ accounts_mutator->RemoveAccount(
+ signed_out_account, signin_metrics::SourceForRefreshTokenOperation::
+ kDiceResponseHandler_Signout);
+ }
+
+ // If a token fetch is in flight for the same account, cancel it.
+ for (auto it = token_fetchers_.begin(); it != token_fetchers_.end(); ++it) {
+ CoreAccountId token_fetcher_account_id =
+ identity_manager_->PickAccountIdForAccount(it->get()->gaia_id(),
+ it->get()->email());
+ if (token_fetcher_account_id == signed_out_account) {
+ token_fetchers_.erase(it);
+ break;
+ }
+ }
+ }
+
+ if (!primary_account_signed_out)
+ RecordDiceResponseHeader(kSignoutSecondary);
+}
+
+void DiceResponseHandler::DeleteTokenFetcher(DiceTokenFetcher* token_fetcher) {
+ for (auto it = token_fetchers_.begin(); it != token_fetchers_.end(); ++it) {
+ if (it->get() == token_fetcher) {
+ token_fetchers_.erase(it);
+ return;
+ }
+ }
+ NOTREACHED();
+}
+
+void DiceResponseHandler::OnTokenExchangeSuccess(
+ DiceTokenFetcher* token_fetcher,
+ const std::string& refresh_token,
+ bool is_under_advanced_protection) {
+ const std::string& email = token_fetcher->email();
+ const std::string& gaia_id = token_fetcher->gaia_id();
+ VLOG(1) << "[Dice] OAuth success for email " << email;
+ bool should_enable_sync = token_fetcher->should_enable_sync();
+ CoreAccountId account_id =
+ identity_manager_->PickAccountIdForAccount(gaia_id, email);
+ bool is_new_account =
+ !identity_manager_->HasAccountWithRefreshToken(account_id);
+ identity_manager_->GetAccountsMutator()->AddOrUpdateAccount(
+ gaia_id, email, refresh_token, is_under_advanced_protection,
+ signin_metrics::SourceForRefreshTokenOperation::
+ kDiceResponseHandler_Signin);
+ about_signin_internals_->OnRefreshTokenReceived(
+ base::StringPrintf("Successful (%s)", account_id.ToString().c_str()));
+ token_fetcher->delegate()->HandleTokenExchangeSuccess(account_id,
+ is_new_account);
+ if (should_enable_sync)
+ token_fetcher->delegate()->EnableSync(account_id);
+
+ DeleteTokenFetcher(token_fetcher);
+}
+
+void DiceResponseHandler::OnTokenExchangeFailure(
+ DiceTokenFetcher* token_fetcher,
+ const GoogleServiceAuthError& error) {
+ const std::string& email = token_fetcher->email();
+ const std::string& gaia_id = token_fetcher->gaia_id();
+ CoreAccountId account_id =
+ identity_manager_->PickAccountIdForAccount(gaia_id, email);
+ about_signin_internals_->OnRefreshTokenReceived(
+ base::StringPrintf("Failure (%s)", account_id.ToString().c_str()));
+ token_fetcher->delegate()->HandleTokenExchangeFailure(email, error);
+
+ DeleteTokenFetcher(token_fetcher);
+}
diff --git a/chromium/chrome/browser/signin/dice_response_handler.h b/chromium/chrome/browser/signin/dice_response_handler.h
new file mode 100644
index 00000000000..2e90c9ddc11
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_response_handler.h
@@ -0,0 +1,187 @@
+// Copyright 2017 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_SIGNIN_DICE_RESPONSE_HANDLER_H_
+#define CHROME_BROWSER_SIGNIN_DICE_RESPONSE_HANDLER_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/cancelable_callback.h"
+#include "base/files/file_path.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/timer/timer.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "components/signin/core/browser/account_reconcilor.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "components/signin/public/base/account_consistency_method.h"
+#include "google_apis/gaia/gaia_auth_consumer.h"
+
+class AboutSigninInternals;
+class GaiaAuthFetcher;
+class GoogleServiceAuthError;
+class SigninClient;
+class Profile;
+
+namespace signin {
+class IdentityManager;
+}
+
+// Exposed for testing.
+extern const int kDiceTokenFetchTimeoutSeconds;
+// Exposed for testing.
+extern const int kLockAccountReconcilorTimeoutHours;
+extern const base::Feature kSupportOAuthOutageInDice;
+
+// Delegate interface for processing a dice request.
+class ProcessDiceHeaderDelegate {
+ public:
+ virtual ~ProcessDiceHeaderDelegate() = default;
+
+ // Called when a token was successfully exchanged.
+ // Called after the account was seeded in the account tracker service and
+ // after the refresh token was fetched and updated in the token service.
+ // |is_new_account| is true if the account was added to Chrome (it is not a
+ // re-auth).
+ virtual void HandleTokenExchangeSuccess(CoreAccountId account_id,
+ bool is_new_account) = 0;
+
+ // Asks the delegate to enable sync for the |account_id|.
+ // Called after the account was seeded in the account tracker service and
+ // after the refresh token was fetched and updated in the token service.
+ virtual void EnableSync(const CoreAccountId& account_id) = 0;
+
+ // Handles a failure in the token exchange (i.e. shows the error to the user).
+ virtual void HandleTokenExchangeFailure(
+ const std::string& email,
+ const GoogleServiceAuthError& error) = 0;
+};
+
+// Processes the Dice responses from Gaia.
+class DiceResponseHandler : public KeyedService {
+ public:
+ // Returns the DiceResponseHandler associated with this profile.
+ // May return nullptr if there is none (e.g. in incognito).
+ static DiceResponseHandler* GetForProfile(Profile* profile);
+
+ DiceResponseHandler(SigninClient* signin_client,
+ signin::IdentityManager* identity_manager,
+ AccountReconcilor* account_reconcilor,
+ AboutSigninInternals* about_signin_internals,
+ const base::FilePath& profile_path_);
+
+ DiceResponseHandler(const DiceResponseHandler&) = delete;
+ DiceResponseHandler& operator=(const DiceResponseHandler&) = delete;
+
+ ~DiceResponseHandler() override;
+
+ // Must be called when receiving a Dice response header.
+ void ProcessDiceHeader(const signin::DiceResponseParams& dice_params,
+ std::unique_ptr<ProcessDiceHeaderDelegate> delegate);
+
+ // Returns the number of pending DiceTokenFetchers. Exposed for testing.
+ size_t GetPendingDiceTokenFetchersCountForTesting() const;
+
+ // Sets |task_runner_| for testing.
+ void SetTaskRunner(scoped_refptr<base::SequencedTaskRunner> task_runner);
+
+ private:
+ // Helper class to fetch a refresh token from an authorization code.
+ class DiceTokenFetcher : public GaiaAuthConsumer {
+ public:
+ DiceTokenFetcher(const std::string& gaia_id,
+ const std::string& email,
+ const std::string& authorization_code,
+ SigninClient* signin_client,
+ AccountReconcilor* account_reconcilor,
+ std::unique_ptr<ProcessDiceHeaderDelegate> delegate,
+ DiceResponseHandler* dice_response_handler);
+
+ DiceTokenFetcher(const DiceTokenFetcher&) = delete;
+ DiceTokenFetcher& operator=(const DiceTokenFetcher&) = delete;
+
+ ~DiceTokenFetcher() override;
+
+ const std::string& gaia_id() const { return gaia_id_; }
+ const std::string& email() const { return email_; }
+ const std::string& authorization_code() const {
+ return authorization_code_;
+ }
+ bool should_enable_sync() const { return should_enable_sync_; }
+ void set_should_enable_sync(bool should_enable_sync) {
+ should_enable_sync_ = should_enable_sync;
+ }
+ ProcessDiceHeaderDelegate* delegate() { return delegate_.get(); }
+
+ private:
+ // Called by |timeout_closure_| when the request times out.
+ void OnTimeout();
+
+ // GaiaAuthConsumer implementation:
+ void OnClientOAuthSuccess(
+ const GaiaAuthConsumer::ClientOAuthResult& result) override;
+ void OnClientOAuthFailure(const GoogleServiceAuthError& error) override;
+
+ // Lock the account reconcilor while tokens are being fetched.
+ std::unique_ptr<AccountReconcilor::Lock> account_reconcilor_lock_;
+
+ std::string gaia_id_;
+ std::string email_;
+ std::string authorization_code_;
+ std::unique_ptr<ProcessDiceHeaderDelegate> delegate_;
+ raw_ptr<DiceResponseHandler> dice_response_handler_;
+ base::CancelableOnceClosure timeout_closure_;
+ bool should_enable_sync_;
+ std::unique_ptr<GaiaAuthFetcher> gaia_auth_fetcher_;
+ };
+
+ // Deletes the token fetcher.
+ void DeleteTokenFetcher(DiceTokenFetcher* token_fetcher);
+
+ // Process the Dice signin action.
+ void ProcessDiceSigninHeader(
+ const std::string& gaia_id,
+ const std::string& email,
+ const std::string& authorization_code,
+ bool no_authorization_code,
+ std::unique_ptr<ProcessDiceHeaderDelegate> delegate);
+
+ // Process the Dice enable sync action.
+ void ProcessEnableSyncHeader(
+ const std::string& gaia_id,
+ const std::string& email,
+ std::unique_ptr<ProcessDiceHeaderDelegate> delegate);
+
+ // Process the Dice signout action.
+ void ProcessDiceSignoutHeader(
+ const std::vector<signin::DiceResponseParams::AccountInfo>&
+ account_infos);
+
+ // Called after exchanging an OAuth 2.0 authorization code for a refresh token
+ // after DiceAction::SIGNIN.
+ void OnTokenExchangeSuccess(DiceTokenFetcher* token_fetcher,
+ const std::string& refresh_token,
+ bool is_under_advanced_protection);
+ void OnTokenExchangeFailure(DiceTokenFetcher* token_fetcher,
+ const GoogleServiceAuthError& error);
+ // Called to unlock the reconcilor after a SLO outage.
+ void OnTimeoutUnlockReconcilor();
+
+ raw_ptr<SigninClient> signin_client_;
+ raw_ptr<signin::IdentityManager> identity_manager_;
+ raw_ptr<AccountReconcilor> account_reconcilor_;
+ raw_ptr<AboutSigninInternals> about_signin_internals_;
+ base::FilePath profile_path_;
+ std::vector<std::unique_ptr<DiceTokenFetcher>> token_fetchers_;
+ // Lock the account reconcilor for kLockAccountReconcilorTimeoutHours
+ // when there was OAuth outage in Dice.
+ std::unique_ptr<AccountReconcilor::Lock> lock_;
+ std::unique_ptr<base::OneShotTimer> timer_;
+ scoped_refptr<base::SequencedTaskRunner> task_runner_;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_DICE_RESPONSE_HANDLER_H_
diff --git a/chromium/chrome/browser/signin/dice_response_handler_unittest.cc b/chromium/chrome/browser/signin/dice_response_handler_unittest.cc
new file mode 100644
index 00000000000..437f4a4fb20
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_response_handler_unittest.cc
@@ -0,0 +1,820 @@
+// Copyright 2017 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.
+
+#include "chrome/browser/signin/dice_response_handler.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/check.h"
+#include "base/command_line.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/ref_counted.h"
+#include "base/notreached.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/test/task_environment.h"
+#include "base/time/time.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/signin/core/browser/about_signin_internals.h"
+#include "components/signin/core/browser/account_reconcilor.h"
+#include "components/signin/core/browser/dice_account_reconcilor_delegate.h"
+#include "components/signin/core/browser/signin_error_controller.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "components/signin/public/base/test_signin_client.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "components/sync_preferences/testing_pref_service_syncable.h"
+#include "content/public/test/browser_task_environment.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using signin::DiceAction;
+using signin::DiceResponseParams;
+
+namespace {
+
+const char kAuthorizationCode[] = "authorization_code";
+const char kEmail[] = "test@email.com";
+const int kSessionIndex = 42;
+
+// TestSigninClient implementation that intercepts the GaiaAuthConsumer and
+// replaces it by a dummy one.
+class DiceTestSigninClient : public TestSigninClient, public GaiaAuthConsumer {
+ public:
+ explicit DiceTestSigninClient(PrefService* pref_service)
+ : TestSigninClient(pref_service), consumer_(nullptr) {}
+
+ DiceTestSigninClient(const DiceTestSigninClient&) = delete;
+ DiceTestSigninClient& operator=(const DiceTestSigninClient&) = delete;
+
+ ~DiceTestSigninClient() override {}
+
+ std::unique_ptr<GaiaAuthFetcher> CreateGaiaAuthFetcher(
+ GaiaAuthConsumer* consumer,
+ gaia::GaiaSource source) override {
+ DCHECK(!consumer_ || (consumer_ == consumer));
+ consumer_ = consumer;
+
+ // Pass |this| as a dummy consumer to CreateGaiaAuthFetcher().
+ // Since DiceTestSigninClient does not overrides any consumer method,
+ // everything will be dropped on the floor.
+ return TestSigninClient::CreateGaiaAuthFetcher(this, source);
+ }
+
+ // We want to reset |consumer_| here before the test interacts with the last
+ // consumer. Interacting with the last consumer (simulating success of the
+ // fetcher) namely sometimes immediately triggers another fetch with another
+ // consumer. If |consumer_| is non-null, we would hit the DCHECK.
+ GaiaAuthConsumer* GetAndClearConsumer() {
+ GaiaAuthConsumer* last_consumer = consumer_;
+ consumer_ = nullptr;
+ return last_consumer;
+ }
+
+ private:
+ raw_ptr<GaiaAuthConsumer> consumer_;
+};
+
+class DiceResponseHandlerTest : public testing::Test,
+ public AccountReconcilor::Observer {
+ public:
+ // Called after the refresh token was fetched and added in the token service.
+ void HandleTokenExchangeSuccess(CoreAccountId account_id,
+ bool is_new_account) {
+ token_exchange_account_id_ = account_id;
+ token_exchange_is_new_account_ = is_new_account;
+ }
+
+ // Called after the refresh token was fetched and added in the token service.
+ void EnableSync(const CoreAccountId& account_id) {
+ enable_sync_account_id_ = account_id;
+ }
+
+ void HandleTokenExchangeFailure(const std::string& email,
+ const GoogleServiceAuthError& error) {
+ auth_error_email_ = email;
+ auth_error_ = error;
+ }
+
+ protected:
+ DiceResponseHandlerTest()
+ : task_environment_(
+ base::test::SingleThreadTaskEnvironment::MainThreadType::IO,
+ base::test::SingleThreadTaskEnvironment::TimeSource::
+ MOCK_TIME), // URLRequestContext requires IO.
+ signin_client_(&pref_service_),
+ identity_test_env_(/*test_url_loader_factory=*/nullptr,
+ &pref_service_,
+ signin::AccountConsistencyMethod::kDice,
+ &signin_client_),
+ signin_error_controller_(
+ SigninErrorController::AccountMode::PRIMARY_ACCOUNT,
+ identity_test_env_.identity_manager()) {
+ EXPECT_TRUE(temp_dir_.CreateUniqueTempDir());
+ AboutSigninInternals::RegisterPrefs(pref_service_.registry());
+ auto account_reconcilor_delegate =
+ std::make_unique<signin::DiceAccountReconcilorDelegate>();
+ account_reconcilor_ = std::make_unique<AccountReconcilor>(
+ identity_test_env_.identity_manager(), &signin_client_,
+ std::move(account_reconcilor_delegate));
+ account_reconcilor_->AddObserver(this);
+
+ about_signin_internals_ = std::make_unique<AboutSigninInternals>(
+ identity_test_env_.identity_manager(), &signin_error_controller_,
+ signin::AccountConsistencyMethod::kDice, &signin_client_,
+ account_reconcilor_.get());
+
+ dice_response_handler_ = std::make_unique<DiceResponseHandler>(
+ &signin_client_, identity_test_env_.identity_manager(),
+ account_reconcilor_.get(), about_signin_internals_.get(),
+ temp_dir_.GetPath());
+ }
+
+ ~DiceResponseHandlerTest() override {
+ account_reconcilor_->RemoveObserver(this);
+ account_reconcilor_->Shutdown();
+ about_signin_internals_->Shutdown();
+ signin_error_controller_.Shutdown();
+ }
+
+ DiceResponseParams MakeDiceParams(DiceAction action) {
+ DiceResponseParams dice_params;
+ dice_params.user_intention = action;
+ DiceResponseParams::AccountInfo account_info;
+ account_info.gaia_id = signin::GetTestGaiaIdForEmail(kEmail);
+ account_info.email = kEmail;
+ account_info.session_index = kSessionIndex;
+ switch (action) {
+ case DiceAction::SIGNIN:
+ dice_params.signin_info =
+ std::make_unique<DiceResponseParams::SigninInfo>();
+ dice_params.signin_info->account_info = account_info;
+ dice_params.signin_info->authorization_code = kAuthorizationCode;
+ break;
+ case DiceAction::ENABLE_SYNC:
+ dice_params.enable_sync_info =
+ std::make_unique<DiceResponseParams::EnableSyncInfo>();
+ dice_params.enable_sync_info->account_info = account_info;
+ break;
+ case DiceAction::SIGNOUT:
+ dice_params.signout_info =
+ std::make_unique<DiceResponseParams::SignoutInfo>();
+ dice_params.signout_info->account_infos.push_back(account_info);
+ break;
+ case DiceAction::NONE:
+ NOTREACHED();
+ break;
+ }
+ return dice_params;
+ }
+
+ // AccountReconcilor::Observer:
+ void OnBlockReconcile() override { ++reconcilor_blocked_count_; }
+ void OnUnblockReconcile() override { ++reconcilor_unblocked_count_; }
+
+ signin::IdentityManager* identity_manager() {
+ return identity_test_env_.identity_manager();
+ }
+
+ base::test::SingleThreadTaskEnvironment task_environment_;
+ base::ScopedTempDir temp_dir_;
+ sync_preferences::TestingPrefServiceSyncable pref_service_;
+ DiceTestSigninClient signin_client_;
+ signin::IdentityTestEnvironment identity_test_env_;
+ SigninErrorController signin_error_controller_;
+ std::unique_ptr<AboutSigninInternals> about_signin_internals_;
+ std::unique_ptr<AccountReconcilor> account_reconcilor_;
+ std::unique_ptr<DiceResponseHandler> dice_response_handler_;
+ int reconcilor_blocked_count_ = 0;
+ int reconcilor_unblocked_count_ = 0;
+ CoreAccountId token_exchange_account_id_;
+ bool token_exchange_is_new_account_ = false;
+ CoreAccountId enable_sync_account_id_;
+ GoogleServiceAuthError auth_error_;
+ std::string auth_error_email_;
+};
+
+class TestProcessDiceHeaderDelegate : public ProcessDiceHeaderDelegate {
+ public:
+ explicit TestProcessDiceHeaderDelegate(DiceResponseHandlerTest* owner)
+ : owner_(owner) {}
+
+ ~TestProcessDiceHeaderDelegate() override = default;
+
+ // Called after the refresh token was fetched and added in the token service.
+ void HandleTokenExchangeSuccess(CoreAccountId account_id,
+ bool is_new_account) override {
+ owner_->HandleTokenExchangeSuccess(account_id, is_new_account);
+ }
+
+ // Called after the refresh token was fetched and added in the token service.
+ void EnableSync(const CoreAccountId& account_id) override {
+ owner_->EnableSync(account_id);
+ }
+
+ void HandleTokenExchangeFailure(
+ const std::string& email,
+ const GoogleServiceAuthError& error) override {
+ owner_->HandleTokenExchangeFailure(email, error);
+ }
+
+ private:
+ raw_ptr<DiceResponseHandlerTest> owner_;
+};
+
+// Checks that a SIGNIN action triggers a token exchange request.
+TEST_F(DiceResponseHandlerTest, Signin) {
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
+ const auto& account_info = dice_params.signin_info->account_info;
+ CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
+ account_info.gaia_id, account_info.email);
+ EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that a GaiaAuthFetcher has been created.
+ GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer, testing::NotNull());
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ // Simulate GaiaAuthFetcher success.
+ consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
+ "refresh_token", "access_token", 10, false /* is_child_account */,
+ true /* is_advanced_protection*/));
+ // Check that the token has been inserted in the token service.
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ EXPECT_TRUE(auth_error_email_.empty());
+ EXPECT_EQ(GoogleServiceAuthError::NONE, auth_error_.state());
+ // Check HandleTokenExchangeSuccess parameters.
+ EXPECT_EQ(token_exchange_account_id_, account_id);
+ EXPECT_TRUE(token_exchange_is_new_account_);
+ // Check that the reconcilor was blocked and unblocked exactly once.
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(1, reconcilor_unblocked_count_);
+ // Check that the AccountInfo::is_under_advanced_protection is set.
+ EXPECT_TRUE(identity_manager()
+ ->FindExtendedAccountInfoByAccountId(account_id)
+ .is_under_advanced_protection);
+}
+
+// Checks that the account reconcilor is blocked when where was OAuth
+// outage in Dice, and unblocked after the timeout.
+TEST_F(DiceResponseHandlerTest, SupportOAuthOutageInDice) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndEnableFeature(kSupportOAuthOutageInDice);
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
+ dice_params.signin_info->authorization_code.clear();
+ dice_params.signin_info->no_authorization_code = true;
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that the reconcilor was blocked and not unblocked before timeout.
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ task_environment_.FastForwardBy(
+ base::Hours(kLockAccountReconcilorTimeoutHours + 1));
+ // Check that the reconcilor was unblocked.
+ EXPECT_EQ(1, reconcilor_unblocked_count_);
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+}
+
+// Check that after receiving two headers with no authorization code,
+// timeout still restarts.
+TEST_F(DiceResponseHandlerTest, CheckTimersDuringOutageinDice) {
+ ASSERT_GT(kLockAccountReconcilorTimeoutHours, 3);
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndEnableFeature(kSupportOAuthOutageInDice);
+ // Create params for the first header with no authorization code.
+ DiceResponseParams dice_params_1 = MakeDiceParams(DiceAction::SIGNIN);
+ dice_params_1.signin_info->authorization_code.clear();
+ dice_params_1.signin_info->no_authorization_code = true;
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params_1, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that the reconcilor was blocked and not unblocked before timeout.
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ // Wait half of the timeout.
+ task_environment_.FastForwardBy(
+ base::Hours(kLockAccountReconcilorTimeoutHours / 2));
+ // Create params for the second header with no authorization code.
+ DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN);
+ dice_params_2.signin_info->authorization_code.clear();
+ dice_params_2.signin_info->no_authorization_code = true;
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params_2, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ task_environment_.FastForwardBy(
+ base::Hours((kLockAccountReconcilorTimeoutHours + 1) / 2 + 1));
+ // Check that the reconcilor was not unblocked after the first timeout
+ // passed, timer should be restarted after getting the second header.
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ task_environment_.FastForwardBy(
+ base::Hours((kLockAccountReconcilorTimeoutHours + 1) / 2));
+ // Check that the reconcilor was unblocked.
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(1, reconcilor_unblocked_count_);
+}
+
+// Check that signin works normally (the token is fetched and added to chrome)
+// on valid headers after getting a no_authorization_code header.
+TEST_F(DiceResponseHandlerTest, CheckSigninAfterOutageInDice) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndEnableFeature(kSupportOAuthOutageInDice);
+ // Create params for the header with no authorization code.
+ DiceResponseParams dice_params_1 = MakeDiceParams(DiceAction::SIGNIN);
+ dice_params_1.signin_info->authorization_code.clear();
+ dice_params_1.signin_info->no_authorization_code = true;
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params_1, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Create params for the valid header with an authorization code.
+ DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN);
+ const auto& account_info_2 = dice_params_2.signin_info->account_info;
+ CoreAccountId account_id_2 = identity_manager()->PickAccountIdForAccount(
+ account_info_2.gaia_id, account_info_2.email);
+ EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_2));
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params_2, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that the reconcilor was blocked and not unblocked before timeout.
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ // Check that a GaiaAuthFetcher has been created.
+ GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer, testing::NotNull());
+ // Simulate GaiaAuthFetcher success.
+ consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
+ "refresh_token", "access_token", 10, false /* is_child_account */,
+ true /* is_advanced_protection*/));
+ // Check that the token has been inserted in the token service.
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_2));
+ EXPECT_TRUE(auth_error_email_.empty());
+ EXPECT_EQ(GoogleServiceAuthError::NONE, auth_error_.state());
+ // Check HandleTokenExchangeSuccess parameters.
+ EXPECT_EQ(token_exchange_account_id_, account_id_2);
+ EXPECT_TRUE(token_exchange_is_new_account_);
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ // Check that the AccountInfo::is_under_advanced_protection is set.
+ EXPECT_TRUE(identity_manager()
+ ->FindExtendedAccountInfoByAccountId(account_id_2)
+ .is_under_advanced_protection);
+ task_environment_.FastForwardBy(
+ base::Hours(kLockAccountReconcilorTimeoutHours + 1));
+ // Check that the reconcilor was unblocked.
+ EXPECT_EQ(1, reconcilor_unblocked_count_);
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+}
+
+// Checks that a SIGNIN action triggers a token exchange request when the
+// account is in authentication error.
+TEST_F(DiceResponseHandlerTest, Reauth) {
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
+ AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable(
+ dice_params.signin_info->account_info.email, signin::ConsentLevel::kSync);
+ dice_params.signin_info->account_info.gaia_id = account_info.gaia;
+ CoreAccountId account_id = account_info.account_id;
+ identity_test_env_.UpdatePersistentErrorOfRefreshTokenForAccount(
+ account_id,
+ GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_id));
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that a GaiaAuthFetcher has been created.
+ GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer, testing::NotNull());
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ // Simulate GaiaAuthFetcher success.
+ consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
+ "refresh_token", "access_token", 10, false /* is_child_account */,
+ true /* is_advanced_protection*/));
+ // Check that the token has been inserted in the token service.
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ EXPECT_FALSE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_id));
+ // Check HandleTokenExchangeSuccess parameters.
+ EXPECT_EQ(token_exchange_account_id_, account_id);
+ EXPECT_FALSE(token_exchange_is_new_account_);
+}
+
+// Checks that a GaiaAuthFetcher failure is handled correctly.
+TEST_F(DiceResponseHandlerTest, SigninFailure) {
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
+ const auto& account_info = dice_params.signin_info->account_info;
+ CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
+ account_info.gaia_id, account_info.email);
+ EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that a GaiaAuthFetcher has been created.
+ GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer, testing::NotNull());
+ EXPECT_EQ(
+ 1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
+ // Simulate GaiaAuthFetcher failure.
+ GoogleServiceAuthError::State error_state =
+ GoogleServiceAuthError::SERVICE_UNAVAILABLE;
+ consumer->OnClientOAuthFailure(GoogleServiceAuthError(error_state));
+ EXPECT_EQ(
+ 0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
+ // Check that the token has not been inserted in the token service.
+ EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ EXPECT_EQ(account_info.email, auth_error_email_);
+ EXPECT_EQ(error_state, auth_error_.state());
+}
+
+// Checks that a second token for the same account is not requested when a
+// request is already in flight.
+TEST_F(DiceResponseHandlerTest, SigninRepeatedWithSameAccount) {
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
+ const auto& account_info = dice_params.signin_info->account_info;
+ CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
+ account_info.gaia_id, account_info.email);
+ ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that a GaiaAuthFetcher has been created.
+ GaiaAuthConsumer* consumer_1 = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer_1, testing::NotNull());
+ // Start a second request for the same account.
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that there is no new request.
+ GaiaAuthConsumer* consumer_2 = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer_2, testing::IsNull());
+ // Simulate GaiaAuthFetcher success for the first request.
+ consumer_1->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
+ "refresh_token", "access_token", 10, false /* is_child_account */,
+ false /* is_advanced_protection*/));
+ // Check that the token has been inserted in the token service.
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ EXPECT_FALSE(identity_manager()
+ ->FindExtendedAccountInfoByAccountId(account_id)
+ .is_under_advanced_protection);
+}
+
+// Checks that two SIGNIN requests can happen concurrently.
+TEST_F(DiceResponseHandlerTest, SigninWithTwoAccounts) {
+ DiceResponseParams dice_params_1 = MakeDiceParams(DiceAction::SIGNIN);
+ const auto& account_info_1 = dice_params_1.signin_info->account_info;
+ DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN);
+ dice_params_2.signin_info->account_info.email = "other_email";
+ dice_params_2.signin_info->account_info.gaia_id = "other_gaia_id";
+ const auto& account_info_2 = dice_params_2.signin_info->account_info;
+ CoreAccountId account_id_1 = identity_manager()->PickAccountIdForAccount(
+ account_info_1.gaia_id, account_info_1.email);
+ CoreAccountId account_id_2 = identity_manager()->PickAccountIdForAccount(
+ account_info_2.gaia_id, account_info_2.email);
+ ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_1));
+ ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_2));
+ // Start first request.
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params_1, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that a GaiaAuthFetcher has been created.
+ GaiaAuthConsumer* consumer_1 = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer_1, testing::NotNull());
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+ // Start second request.
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params_2, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ GaiaAuthConsumer* consumer_2 = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer_2, testing::NotNull());
+ // Simulate GaiaAuthFetcher success for the first request.
+ consumer_1->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
+ "refresh_token", "access_token", 10, false /* is_child_account */,
+ true /* is_advanced_protection*/));
+ // Check that the token has been inserted in the token service.
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_1));
+ EXPECT_TRUE(identity_manager()
+ ->FindExtendedAccountInfoByAccountId(account_id_1)
+ .is_under_advanced_protection);
+ // Simulate GaiaAuthFetcher success for the second request.
+ consumer_2->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
+ "refresh_token", "access_token", 10, false /* is_child_account */,
+ false /* is_advanced_protection*/));
+ // Check that the token has been inserted in the token service.
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_2));
+ EXPECT_FALSE(identity_manager()
+ ->FindExtendedAccountInfoByAccountId(account_id_2)
+ .is_under_advanced_protection);
+ // Check that the reconcilor was blocked and unblocked exactly once.
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(1, reconcilor_unblocked_count_);
+}
+
+// Checks that a ENABLE_SYNC action received after the refresh token is added
+// to the token service, triggers a call to enable sync on the delegate.
+TEST_F(DiceResponseHandlerTest, SigninEnableSyncAfterRefreshTokenFetched) {
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
+ const auto& account_info = dice_params.signin_info->account_info;
+ CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
+ account_info.gaia_id, account_info.email);
+ ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that a GaiaAuthFetcher has been created.
+ GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer, testing::NotNull());
+ // Simulate GaiaAuthFetcher success.
+ consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
+ "refresh_token", "access_token", 10, false /* is_child_account */,
+ false /* is_advanced_protection*/));
+ // Check that the token has been inserted in the token service.
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ // Check HandleTokenExchangeSuccess parameters.
+ EXPECT_EQ(token_exchange_account_id_, account_id);
+ EXPECT_TRUE(token_exchange_is_new_account_);
+ // Check that delegate was not called to enable sync.
+ EXPECT_TRUE(enable_sync_account_id_.empty());
+
+ // Enable sync.
+ dice_response_handler_->ProcessDiceHeader(
+ MakeDiceParams(DiceAction::ENABLE_SYNC),
+ std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that delegate was called to enable sync.
+ EXPECT_EQ(account_id, enable_sync_account_id_);
+}
+
+// Checks that a ENABLE_SYNC action received before the refresh token is added
+// to the token service, is schedules a call to enable sync on the delegate
+// once the refresh token is received.
+TEST_F(DiceResponseHandlerTest, SigninEnableSyncBeforeRefreshTokenFetched) {
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
+ const auto& account_info = dice_params.signin_info->account_info;
+ CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
+ account_info.gaia_id, account_info.email);
+ ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that a GaiaAuthFetcher has been created.
+ GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer, testing::NotNull());
+
+ // Enable sync.
+ dice_response_handler_->ProcessDiceHeader(
+ MakeDiceParams(DiceAction::ENABLE_SYNC),
+ std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that delegate was not called to enable sync.
+ EXPECT_TRUE(enable_sync_account_id_.empty());
+
+ // Simulate GaiaAuthFetcher success.
+ consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
+ "refresh_token", "access_token", 10, false /* is_child_account */,
+ false /* is_advanced_protection*/));
+ // Check that the token has been inserted in the token service.
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ // Check HandleTokenExchangeSuccess parameters.
+ EXPECT_EQ(token_exchange_account_id_, account_id);
+ EXPECT_TRUE(token_exchange_is_new_account_);
+ // Check that delegate was called to enable sync.
+ EXPECT_EQ(account_id, enable_sync_account_id_);
+}
+
+TEST_F(DiceResponseHandlerTest, Timeout) {
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
+ const auto& account_info = dice_params.signin_info->account_info;
+ CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
+ account_info.gaia_id, account_info.email);
+ ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that a GaiaAuthFetcher has been created.
+ GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer, testing::NotNull());
+ EXPECT_EQ(
+ 1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
+ // Force a timeout.
+ task_environment_.FastForwardBy(
+ base::Seconds(kDiceTokenFetchTimeoutSeconds + 1));
+ EXPECT_EQ(
+ 0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
+ // Check that the token has not been inserted in the token service.
+ EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
+ // Check that the reconcilor was blocked and unblocked exactly once.
+ EXPECT_EQ(1, reconcilor_blocked_count_);
+ EXPECT_EQ(1, reconcilor_unblocked_count_);
+}
+
+TEST_F(DiceResponseHandlerTest, SignoutMainAccount) {
+ const char kSecondaryEmail[] = "other@gmail.com";
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT);
+ const auto& dice_account_info = dice_params.signout_info->account_infos[0];
+ // User is signed in to Chrome, and has some refresh token for a secondary
+ // account.
+ AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable(
+ dice_account_info.email, signin::ConsentLevel::kSync);
+ AccountInfo secondary_account_info =
+ identity_test_env_.MakeAccountAvailable(kSecondaryEmail);
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshToken(account_info.account_id));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
+ secondary_account_info.account_id));
+ EXPECT_TRUE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ // Receive signout response for the main account.
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+
+ // User is not signed out, token for the main account is now invalid,
+ // secondary account is untouched.
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshToken(account_info.account_id));
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_info.account_id));
+ auto error = identity_manager()->GetErrorStateOfRefreshTokenForAccount(
+ account_info.account_id);
+ EXPECT_EQ(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS, error.state());
+ EXPECT_EQ(GoogleServiceAuthError::InvalidGaiaCredentialsReason::
+ CREDENTIALS_REJECTED_BY_CLIENT,
+ error.GetInvalidGaiaCredentialsReason());
+
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
+ secondary_account_info.account_id));
+ EXPECT_FALSE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ secondary_account_info.account_id));
+
+ EXPECT_TRUE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ // Check that the reconcilor was not blocked.
+ EXPECT_EQ(0, reconcilor_blocked_count_);
+ EXPECT_EQ(0, reconcilor_unblocked_count_);
+}
+
+TEST_F(DiceResponseHandlerTest, SignoutSecondaryAccount) {
+ const char kMainEmail[] = "main@gmail.com";
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT);
+ const auto& secondary_dice_account_info =
+ dice_params.signout_info->account_infos[0];
+ // User is signed in to Chrome, and has some refresh token for a secondary
+ // account.
+ AccountInfo main_account_info =
+ identity_test_env_.MakePrimaryAccountAvailable(
+ kMainEmail, signin::ConsentLevel::kSync);
+ AccountInfo secondary_account_info = identity_test_env_.MakeAccountAvailable(
+ secondary_dice_account_info.email);
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
+ secondary_account_info.account_id));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
+ main_account_info.account_id));
+ EXPECT_TRUE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ // Receive signout response for the secondary account.
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+
+ // Only the token corresponding the the Dice parameter has been removed, and
+ // the user is still signed in.
+ EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(
+ secondary_account_info.account_id));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
+ main_account_info.account_id));
+ EXPECT_TRUE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+}
+
+TEST_F(DiceResponseHandlerTest, SignoutWebOnly) {
+ const char kSecondaryEmail[] = "other@gmail.com";
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT);
+ const auto& dice_account_info = dice_params.signout_info->account_infos[0];
+ // User is NOT signed in to Chrome, and has some refresh tokens for two
+ // accounts.
+ AccountInfo account_info =
+ identity_test_env_.MakeAccountAvailable(dice_account_info.email);
+ AccountInfo secondary_account_info =
+ identity_test_env_.MakeAccountAvailable(kSecondaryEmail);
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshToken(account_info.account_id));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
+ secondary_account_info.account_id));
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ // Receive signout response.
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Only the token corresponding the the Dice parameter has been removed.
+ EXPECT_FALSE(
+ identity_manager()->HasAccountWithRefreshToken(account_info.account_id));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
+ secondary_account_info.account_id));
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+}
+
+// Checks that signin in progress is canceled by a signout.
+TEST_F(DiceResponseHandlerTest, SigninSignoutSameAccount) {
+ DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT);
+ const auto& dice_account_info = dice_params.signout_info->account_infos[0];
+
+ // User is signed in to Chrome.
+ AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable(
+ dice_account_info.email, signin::ConsentLevel::kSync);
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshToken(account_info.account_id));
+ EXPECT_FALSE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_info.account_id));
+ // Start Dice signin (reauth).
+ DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN);
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params_2, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that a GaiaAuthFetcher has been created and is pending.
+ ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::NotNull());
+ EXPECT_EQ(
+ 1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
+ // Signout while signin is in flight.
+ dice_response_handler_->ProcessDiceHeader(
+ dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that the token fetcher has been canceled and the token is invalid.
+ EXPECT_EQ(
+ 0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshToken(account_info.account_id));
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_info.account_id));
+ auto error = identity_manager()->GetErrorStateOfRefreshTokenForAccount(
+ account_info.account_id);
+ EXPECT_EQ(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS, error.state());
+ EXPECT_EQ(GoogleServiceAuthError::InvalidGaiaCredentialsReason::
+ CREDENTIALS_REJECTED_BY_CLIENT,
+ error.GetInvalidGaiaCredentialsReason());
+}
+
+// Checks that signin in progress is not canceled by a signout for a different
+// account.
+TEST_F(DiceResponseHandlerTest, SigninSignoutDifferentAccount) {
+ // User starts signin in the web with two accounts.
+ DiceResponseParams signout_params_1 = MakeDiceParams(DiceAction::SIGNOUT);
+ DiceResponseParams signin_params_1 = MakeDiceParams(DiceAction::SIGNIN);
+ DiceResponseParams signin_params_2 = MakeDiceParams(DiceAction::SIGNIN);
+ signin_params_2.signin_info->account_info.email = "other_email";
+ signin_params_2.signin_info->account_info.gaia_id = "other_gaia_id";
+ const auto& signin_account_info_1 = signin_params_1.signin_info->account_info;
+ const auto& signin_account_info_2 = signin_params_2.signin_info->account_info;
+ CoreAccountId account_id_1 = identity_manager()->PickAccountIdForAccount(
+ signin_account_info_1.gaia_id, signin_account_info_1.email);
+ CoreAccountId account_id_2 = identity_manager()->PickAccountIdForAccount(
+ signin_account_info_2.gaia_id, signin_account_info_2.email);
+ dice_response_handler_->ProcessDiceHeader(
+ signin_params_1, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+
+ GaiaAuthConsumer* consumer_1 = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer_1, testing::NotNull());
+ dice_response_handler_->ProcessDiceHeader(
+ signin_params_2, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ GaiaAuthConsumer* consumer_2 = signin_client_.GetAndClearConsumer();
+ ASSERT_THAT(consumer_2, testing::NotNull());
+ EXPECT_EQ(
+ 2u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
+ ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_1));
+ ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_2));
+ ASSERT_FALSE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_id_1));
+ ASSERT_FALSE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_id_2));
+ // Signout from one of the accounts while signin is in flight.
+ dice_response_handler_->ProcessDiceHeader(
+ signout_params_1, std::make_unique<TestProcessDiceHeaderDelegate>(this));
+ // Check that one of the fetchers is cancelled.
+ EXPECT_EQ(
+ 1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
+ // Allow the remaining fetcher to complete.
+ consumer_2->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
+ "refresh_token", "access_token", 10, false /* is_child_account */,
+ false /* is_advanced_protection*/));
+ EXPECT_EQ(
+ 0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
+ // Check that the right token is available.
+ EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_1));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_2));
+ EXPECT_FALSE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_id_2));
+}
+
+// Tests that the DiceResponseHandler is created for a normal profile but not
+// for off-the-record profiles.
+TEST(DiceResponseHandlerFactoryTest, NotInOffTheRecord) {
+ content::BrowserTaskEnvironment task_environment;
+ TestingProfile profile;
+ EXPECT_THAT(DiceResponseHandler::GetForProfile(&profile), testing::NotNull());
+ EXPECT_THAT(DiceResponseHandler::GetForProfile(
+ profile.GetPrimaryOTRProfile(/*create_if_needed=*/true)),
+ testing::IsNull());
+ EXPECT_THAT(DiceResponseHandler::GetForProfile(profile.GetOffTheRecordProfile(
+ Profile::OTRProfileID::CreateUniqueForTesting(),
+ /*create_if_needed=*/true)),
+ testing::IsNull());
+}
+
+} // namespace
diff --git a/chromium/chrome/browser/signin/dice_signed_in_profile_creator.cc b/chromium/chrome/browser/signin/dice_signed_in_profile_creator.cc
new file mode 100644
index 00000000000..494a17b4695
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_signed_in_profile_creator.cc
@@ -0,0 +1,211 @@
+// Copyright 2020 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.
+
+#include "chrome/browser/signin/dice_signed_in_profile_creator.h"
+
+#include <string>
+
+#include "base/check.h"
+#include "base/location.h"
+#include "base/memory/ptr_util.h"
+#include "base/memory/raw_ptr.h"
+#include "base/scoped_observation.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_avatar_icon_util.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/signin/public/identity_manager/accounts_mutator.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+
+// Waits until the tokens are loaded and calls the callback. The callback is
+// called immediately if the tokens are already loaded, and called with nullptr
+// if the profile is destroyed before the tokens are loaded.
+class TokensLoadedCallbackRunner : public signin::IdentityManager::Observer {
+ public:
+ ~TokensLoadedCallbackRunner() override = default;
+ TokensLoadedCallbackRunner(const TokensLoadedCallbackRunner&) = delete;
+ TokensLoadedCallbackRunner& operator=(const TokensLoadedCallbackRunner&) =
+ delete;
+
+ // Runs the callback when the tokens are loaded. If tokens are already loaded
+ // the callback is called synchronously and this returns nullptr.
+ static std::unique_ptr<TokensLoadedCallbackRunner> RunWhenLoaded(
+ Profile* profile,
+ base::OnceCallback<void(Profile*)> callback);
+
+ private:
+ TokensLoadedCallbackRunner(Profile* profile,
+ base::OnceCallback<void(Profile*)> callback);
+
+ // signin::IdentityManager::Observer implementation:
+ void OnRefreshTokensLoaded() override {
+ scoped_identity_manager_observer_.Reset();
+ std::move(callback_).Run(profile_.get());
+ }
+
+ void OnIdentityManagerShutdown(signin::IdentityManager* manager) override {
+ scoped_identity_manager_observer_.Reset();
+ std::move(callback_).Run(nullptr);
+ }
+
+ raw_ptr<Profile> profile_;
+ raw_ptr<signin::IdentityManager> identity_manager_;
+ base::ScopedObservation<signin::IdentityManager,
+ signin::IdentityManager::Observer>
+ scoped_identity_manager_observer_{this};
+ base::OnceCallback<void(Profile*)> callback_;
+};
+
+// static
+std::unique_ptr<TokensLoadedCallbackRunner>
+TokensLoadedCallbackRunner::RunWhenLoaded(
+ Profile* profile,
+ base::OnceCallback<void(Profile*)> callback) {
+ if (IdentityManagerFactory::GetForProfile(profile)
+ ->AreRefreshTokensLoaded()) {
+ std::move(callback).Run(profile);
+ return nullptr;
+ }
+
+ return base::WrapUnique(
+ new TokensLoadedCallbackRunner(profile, std::move(callback)));
+}
+
+TokensLoadedCallbackRunner::TokensLoadedCallbackRunner(
+ Profile* profile,
+ base::OnceCallback<void(Profile*)> callback)
+ : profile_(profile),
+ identity_manager_(IdentityManagerFactory::GetForProfile(profile)),
+ callback_(std::move(callback)) {
+ DCHECK(profile_);
+ DCHECK(identity_manager_);
+ DCHECK(callback_);
+ DCHECK(!identity_manager_->AreRefreshTokensLoaded());
+ scoped_identity_manager_observer_.Observe(identity_manager_.get());
+}
+
+DiceSignedInProfileCreator::DiceSignedInProfileCreator(
+ Profile* source_profile,
+ CoreAccountId account_id,
+ const std::u16string& local_profile_name,
+ absl::optional<size_t> icon_index,
+ bool use_guest_profile,
+ base::OnceCallback<void(Profile*)> callback)
+ : source_profile_(source_profile),
+ account_id_(account_id),
+ callback_(std::move(callback)) {
+ // Passing the sign-in token to an ephemeral Guest profile is part of the
+ // experiment to surface a Guest mode link in the DiceWebSigninIntercept
+ // and is only used to sign in to the web through account consistency and
+ // does NOT enable sync or any other browser level functionality.
+ // TODO(https://crbug.com/1225171): Revise the comment after Guest mode plans
+ // are finalized.
+ if (use_guest_profile) {
+ // TODO(https://crbug.com/1225171): Re-enabled if ephemeral based Guest mode
+ // is added. Remove the code otherwise.
+ NOTREACHED();
+
+ // Make sure the callback is not called synchronously.
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&ProfileManager::CreateProfileAsync,
+ base::Unretained(g_browser_process->profile_manager()),
+ ProfileManager::GetGuestProfilePath(),
+ base::BindRepeating(
+ &DiceSignedInProfileCreator::OnNewProfileCreated,
+ weak_pointer_factory_.GetWeakPtr())));
+ } else {
+ ProfileAttributesStorage& storage =
+ g_browser_process->profile_manager()->GetProfileAttributesStorage();
+ if (!icon_index.has_value())
+ icon_index = storage.ChooseAvatarIconIndexForNewProfile();
+ std::u16string name = local_profile_name.empty()
+ ? storage.ChooseNameForNewProfile(*icon_index)
+ : local_profile_name;
+ ProfileManager::CreateMultiProfileAsync(
+ name, *icon_index, /*is_hidden=*/false,
+ base::BindRepeating(&DiceSignedInProfileCreator::OnNewProfileCreated,
+ weak_pointer_factory_.GetWeakPtr()));
+ }
+}
+
+DiceSignedInProfileCreator::DiceSignedInProfileCreator(
+ Profile* source_profile,
+ CoreAccountId account_id,
+ const base::FilePath& target_profile_path,
+ base::OnceCallback<void(Profile*)> callback)
+ : source_profile_(source_profile),
+ account_id_(account_id),
+ callback_(std::move(callback)) {
+ // Make sure the callback is not called synchronously.
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(
+ base::IgnoreResult(&ProfileManager::LoadProfileByPath),
+ base::Unretained(g_browser_process->profile_manager()),
+ target_profile_path, /*incognito=*/false,
+ base::BindOnce(&DiceSignedInProfileCreator::OnNewProfileInitialized,
+ weak_pointer_factory_.GetWeakPtr())));
+}
+
+DiceSignedInProfileCreator::~DiceSignedInProfileCreator() = default;
+
+void DiceSignedInProfileCreator::OnNewProfileCreated(
+ Profile* new_profile,
+ Profile::CreateStatus status) {
+ switch (status) {
+ case Profile::CREATE_STATUS_CREATED:
+ // Ignore this, wait for profile to be initialized.
+ return;
+ case Profile::CREATE_STATUS_INITIALIZED:
+ OnNewProfileInitialized(new_profile);
+ return;
+ case Profile::CREATE_STATUS_LOCAL_FAIL:
+ NOTREACHED() << "Error creating new profile";
+ if (callback_)
+ std::move(callback_).Run(nullptr);
+ return;
+ }
+}
+
+void DiceSignedInProfileCreator::OnNewProfileInitialized(Profile* new_profile) {
+ if (!new_profile) {
+ if (callback_)
+ std::move(callback_).Run(nullptr);
+ return;
+ }
+
+ DCHECK(!tokens_loaded_callback_runner_);
+ // base::Unretained is fine because the runner is owned by this.
+ auto tokens_loaded_callback_runner =
+ TokensLoadedCallbackRunner::RunWhenLoaded(
+ new_profile,
+ base::BindOnce(&DiceSignedInProfileCreator::OnNewProfileTokensLoaded,
+ base::Unretained(this)));
+ // If the callback was called synchronously, |this| may have been deleted.
+ if (tokens_loaded_callback_runner) {
+ tokens_loaded_callback_runner_ = std::move(tokens_loaded_callback_runner);
+ }
+}
+
+void DiceSignedInProfileCreator::OnNewProfileTokensLoaded(
+ Profile* new_profile) {
+ tokens_loaded_callback_runner_.reset();
+ if (!new_profile) {
+ if (callback_)
+ std::move(callback_).Run(nullptr);
+ return;
+ }
+
+ auto* accounts_mutator =
+ IdentityManagerFactory::GetForProfile(source_profile_)
+ ->GetAccountsMutator();
+ auto* new_profile_accounts_mutator =
+ IdentityManagerFactory::GetForProfile(new_profile)->GetAccountsMutator();
+ accounts_mutator->MoveAccount(new_profile_accounts_mutator, account_id_);
+ if (callback_)
+ std::move(callback_).Run(new_profile);
+}
diff --git a/chromium/chrome/browser/signin/dice_signed_in_profile_creator.h b/chromium/chrome/browser/signin/dice_signed_in_profile_creator.h
new file mode 100644
index 00000000000..97bb462da96
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_signed_in_profile_creator.h
@@ -0,0 +1,70 @@
+// Copyright 2020 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_SIGNIN_DICE_SIGNED_IN_PROFILE_CREATOR_H_
+#define CHROME_BROWSER_SIGNIN_DICE_SIGNED_IN_PROFILE_CREATOR_H_
+
+#include <string>
+
+#include "base/callback.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "chrome/browser/profiles/profile.h"
+#include "google_apis/gaia/core_account_id.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+
+class TokensLoadedCallbackRunner;
+
+// Extracts an account from an existing profile and moves it to a new profile.
+class DiceSignedInProfileCreator {
+ public:
+ // Creates a new profile or uses Guest profile if |use_guest_profile|, and
+ // moves the account from source_profile to it.
+ // The callback is called with the new profile or nullptr in case of failure.
+ // The callback is never called synchronously.
+ // If |local_profile_name| is not empty, it will be set as local name for the
+ // new profile.
+ // If |icon_index| is nullopt, a random icon will be selected.
+ DiceSignedInProfileCreator(Profile* source_profile,
+ CoreAccountId account_id,
+ const std::u16string& local_profile_name,
+ absl::optional<size_t> icon_index,
+ bool use_guest_profile,
+ base::OnceCallback<void(Profile*)> callback);
+
+ // Uses this version when the profile already exists at `target_profile_path`
+ // but may not be loaded in memory. The profile is loaded if necessary, and
+ // the account is moved.
+ DiceSignedInProfileCreator(Profile* source_profile,
+ CoreAccountId account_id,
+ const base::FilePath& target_profile_path,
+ base::OnceCallback<void(Profile*)> callback);
+
+ ~DiceSignedInProfileCreator();
+
+ DiceSignedInProfileCreator(const DiceSignedInProfileCreator&) = delete;
+ DiceSignedInProfileCreator& operator=(const DiceSignedInProfileCreator&) =
+ delete;
+
+ private:
+ // Callback invoked once a profile is created, so we can transfer the
+ // credentials.
+ void OnNewProfileCreated(Profile* new_profile, Profile::CreateStatus status);
+
+ // Called when the profile is initialized.
+ void OnNewProfileInitialized(Profile* new_profile);
+
+ // Callback invoked once the token service is ready for the new profile.
+ void OnNewProfileTokensLoaded(Profile* new_profile);
+
+ const raw_ptr<Profile> source_profile_;
+ const CoreAccountId account_id_;
+
+ base::OnceCallback<void(Profile*)> callback_;
+ std::unique_ptr<TokensLoadedCallbackRunner> tokens_loaded_callback_runner_;
+
+ base::WeakPtrFactory<DiceSignedInProfileCreator> weak_pointer_factory_{this};
+};
+
+#endif // CHROME_BROWSER_SIGNIN_DICE_SIGNED_IN_PROFILE_CREATOR_H_
diff --git a/chromium/chrome/browser/signin/dice_signed_in_profile_creator_unittest.cc b/chromium/chrome/browser/signin/dice_signed_in_profile_creator_unittest.cc
new file mode 100644
index 00000000000..b0d813a17ec
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_signed_in_profile_creator_unittest.cc
@@ -0,0 +1,285 @@
+// Copyright 2020 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.
+
+#include "chrome/browser/signin/dice_signed_in_profile_creator.h"
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/memory/raw_ptr.h"
+#include "base/run_loop.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_avatar_icon_util.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/profiles/profile_manager_observer.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
+#include "chrome/test/base/fake_profile_manager.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+const char16_t kProfileTestName[] = u"profile_test_name";
+
+std::unique_ptr<TestingProfile> BuildTestingProfile(const base::FilePath& path,
+ Profile::Delegate* delegate,
+ bool tokens_loaded) {
+ TestingProfile::Builder profile_builder;
+ profile_builder.SetDelegate(delegate);
+ profile_builder.SetPath(path);
+ std::unique_ptr<TestingProfile> profile =
+ IdentityTestEnvironmentProfileAdaptor::
+ CreateProfileForIdentityTestEnvironment(profile_builder);
+ if (!tokens_loaded) {
+ IdentityTestEnvironmentProfileAdaptor adaptor(profile.get());
+ adaptor.identity_test_env()->ResetToAccountsNotYetLoadedFromDiskState();
+ }
+ if (profile->GetPath() == ProfileManager::GetGuestProfilePath())
+ profile->SetGuestSession(true);
+ return profile;
+}
+
+class UnittestProfileManager : public FakeProfileManager {
+ public:
+ explicit UnittestProfileManager(const base::FilePath& user_data_dir)
+ : FakeProfileManager(user_data_dir) {}
+
+ void set_tokens_loaded_at_creation(bool loaded) {
+ tokens_loaded_at_creation_ = loaded;
+ }
+
+ std::unique_ptr<TestingProfile> BuildTestingProfile(
+ const base::FilePath& path,
+ Profile::Delegate* delegate) override {
+ return ::BuildTestingProfile(path, delegate, tokens_loaded_at_creation_);
+ }
+
+ bool tokens_loaded_at_creation_ = true;
+};
+
+} // namespace
+
+class DiceSignedInProfileCreatorTest : public testing::Test,
+ public ProfileManagerObserver {
+ public:
+ DiceSignedInProfileCreatorTest()
+ : local_state_(TestingBrowserProcess::GetGlobal()) {
+ EXPECT_TRUE(temp_dir_.CreateUniqueTempDir());
+ auto profile_manager_unique =
+ std::make_unique<UnittestProfileManager>(temp_dir_.GetPath());
+ profile_manager_ = profile_manager_unique.get();
+ TestingBrowserProcess::GetGlobal()->SetProfileManager(
+ std::move(profile_manager_unique));
+ profile_ = BuildTestingProfile(base::FilePath(), /*delegate=*/nullptr,
+ /*tokens_loaded=*/true);
+ identity_test_env_profile_adaptor_ =
+ std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile());
+ profile_manager()->AddObserver(this);
+ }
+
+ ~DiceSignedInProfileCreatorTest() override { DeleteProfiles(); }
+
+ UnittestProfileManager* profile_manager() { return profile_manager_; }
+
+ // Test environment attached to profile().
+ signin::IdentityTestEnvironment* identity_test_env() {
+ return identity_test_env_profile_adaptor_->identity_test_env();
+ }
+
+ // Source profile (the one which we are extracting credentials from).
+ Profile* profile() { return profile_.get(); }
+
+ // Profile created by the DiceSignedInProfileCreator.
+ Profile* signed_in_profile() { return signed_in_profile_; }
+
+ // Profile added to the ProfileManager. In general this should be the same as
+ // signed_in_profile() except in error cases.
+ Profile* added_profile() { return added_profile_; }
+
+ bool creator_callback_called() { return creator_callback_called_; }
+
+ void set_profile_added_closure(base::OnceClosure closure) {
+ profile_added_closure_ = std::move(closure);
+ }
+
+ bool use_guest_profile() const { return use_guest_profile_; }
+
+ void DeleteProfiles() {
+ identity_test_env_profile_adaptor_.reset();
+ if (profile_manager_) {
+ profile_manager()->RemoveObserver(this);
+ TestingBrowserProcess::GetGlobal()->SetProfileManager(nullptr);
+ profile_manager_ = nullptr;
+ }
+ }
+
+ // Callback for the DiceSignedInProfileCreator.
+ void OnProfileCreated(base::OnceClosure quit_closure, Profile* profile) {
+ creator_callback_called_ = true;
+ signed_in_profile_ = profile;
+ if (quit_closure)
+ std::move(quit_closure).Run();
+ }
+
+ // ProfileManagerObserver:
+ void OnProfileAdded(Profile* profile) override {
+ added_profile_ = profile;
+ if (profile_added_closure_)
+ std::move(profile_added_closure_).Run();
+ }
+
+ private:
+ content::BrowserTaskEnvironment task_environment_;
+ base::ScopedTempDir temp_dir_;
+ ScopedTestingLocalState local_state_;
+ raw_ptr<UnittestProfileManager> profile_manager_ = nullptr;
+ std::unique_ptr<IdentityTestEnvironmentProfileAdaptor>
+ identity_test_env_profile_adaptor_;
+ std::unique_ptr<TestingProfile> profile_;
+ raw_ptr<Profile> signed_in_profile_ = nullptr;
+ raw_ptr<Profile> added_profile_ = nullptr;
+ base::OnceClosure profile_added_closure_;
+ bool creator_callback_called_ = false;
+ base::test::ScopedFeatureList scoped_feature_list_;
+ bool use_guest_profile_ = false;
+};
+
+TEST_F(DiceSignedInProfileCreatorTest, CreateWithTokensLoaded) {
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ size_t kTestIcon = profiles::GetModernAvatarIconStartIndex();
+
+ base::RunLoop loop;
+ std::unique_ptr<DiceSignedInProfileCreator> creator =
+ std::make_unique<DiceSignedInProfileCreator>(
+ profile(), account_info.account_id, kProfileTestName, kTestIcon,
+ use_guest_profile(),
+ base::BindOnce(&DiceSignedInProfileCreatorTest::OnProfileCreated,
+ base::Unretained(this), loop.QuitClosure()));
+ loop.Run();
+
+ // Check that the account was moved.
+ EXPECT_TRUE(creator_callback_called());
+ EXPECT_TRUE(signed_in_profile());
+ EXPECT_NE(profile(), signed_in_profile());
+ EXPECT_EQ(signed_in_profile(), added_profile());
+ EXPECT_FALSE(IdentityManagerFactory::GetForProfile(profile())
+ ->HasAccountWithRefreshToken(account_info.account_id));
+ EXPECT_EQ(1u, IdentityManagerFactory::GetForProfile(signed_in_profile())
+ ->GetAccountsWithRefreshTokens()
+ .size());
+ EXPECT_TRUE(IdentityManagerFactory::GetForProfile(signed_in_profile())
+ ->HasAccountWithRefreshToken(account_info.account_id));
+
+ // Check profile type
+ ASSERT_EQ(use_guest_profile(), signed_in_profile()->IsGuestSession());
+
+ // Check the profile name and icon.
+ ProfileAttributesStorage& storage =
+ profile_manager()->GetProfileAttributesStorage();
+ ProfileAttributesEntry* entry =
+ storage.GetProfileAttributesWithPath(signed_in_profile()->GetPath());
+ ASSERT_TRUE(entry);
+ if (!use_guest_profile()) {
+ EXPECT_EQ(kProfileTestName, entry->GetLocalProfileName());
+ EXPECT_EQ(kTestIcon, entry->GetAvatarIconIndex());
+ }
+}
+
+TEST_F(DiceSignedInProfileCreatorTest, CreateWithTokensNotLoaded) {
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ profile_manager()->set_tokens_loaded_at_creation(false);
+
+ base::RunLoop creator_loop;
+ base::RunLoop profile_added_loop;
+ set_profile_added_closure(profile_added_loop.QuitClosure());
+ std::unique_ptr<DiceSignedInProfileCreator> creator =
+ std::make_unique<DiceSignedInProfileCreator>(
+ profile(), account_info.account_id, std::u16string(), absl::nullopt,
+ use_guest_profile(),
+ base::BindOnce(&DiceSignedInProfileCreatorTest::OnProfileCreated,
+ base::Unretained(this), creator_loop.QuitClosure()));
+ profile_added_loop.Run();
+ base::RunLoop().RunUntilIdle();
+
+ // The profile was created, but tokens not loaded. The callback has not been
+ // called yet.
+ EXPECT_FALSE(creator_callback_called());
+ EXPECT_TRUE(added_profile());
+ EXPECT_NE(profile(), added_profile());
+
+ // Load the tokens.
+ IdentityTestEnvironmentProfileAdaptor adaptor(added_profile());
+ adaptor.identity_test_env()->ReloadAccountsFromDisk();
+ creator_loop.Run();
+
+ // Check that the account was moved.
+ EXPECT_EQ(signed_in_profile(), added_profile());
+ EXPECT_TRUE(creator_callback_called());
+ EXPECT_FALSE(IdentityManagerFactory::GetForProfile(profile())
+ ->HasAccountWithRefreshToken(account_info.account_id));
+ EXPECT_EQ(1u, IdentityManagerFactory::GetForProfile(signed_in_profile())
+ ->GetAccountsWithRefreshTokens()
+ .size());
+ EXPECT_TRUE(IdentityManagerFactory::GetForProfile(signed_in_profile())
+ ->HasAccountWithRefreshToken(account_info.account_id));
+}
+
+// Deleting the creator while it is running does not crash.
+TEST_F(DiceSignedInProfileCreatorTest, DeleteWhileCreating) {
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ std::unique_ptr<DiceSignedInProfileCreator> creator =
+ std::make_unique<DiceSignedInProfileCreator>(
+ profile(), account_info.account_id, std::u16string(), absl::nullopt,
+ use_guest_profile(),
+ base::BindOnce(&DiceSignedInProfileCreatorTest::OnProfileCreated,
+ base::Unretained(this), base::OnceClosure()));
+ EXPECT_FALSE(creator_callback_called());
+ creator.reset();
+ base::RunLoop().RunUntilIdle();
+}
+
+// Deleting the profile while waiting for the tokens.
+TEST_F(DiceSignedInProfileCreatorTest, DeleteProfile) {
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ profile_manager()->set_tokens_loaded_at_creation(false);
+
+ base::RunLoop creator_loop;
+ base::RunLoop profile_added_loop;
+ set_profile_added_closure(profile_added_loop.QuitClosure());
+ std::unique_ptr<DiceSignedInProfileCreator> creator =
+ std::make_unique<DiceSignedInProfileCreator>(
+ profile(), account_info.account_id, std::u16string(), absl::nullopt,
+ use_guest_profile(),
+ base::BindOnce(&DiceSignedInProfileCreatorTest::OnProfileCreated,
+ base::Unretained(this), creator_loop.QuitClosure()));
+ profile_added_loop.Run();
+ base::RunLoop().RunUntilIdle();
+
+ // The profile was created, but tokens not loaded. The callback has not been
+ // called yet.
+ EXPECT_FALSE(creator_callback_called());
+ EXPECT_TRUE(added_profile());
+ EXPECT_NE(profile(), added_profile());
+
+ DeleteProfiles();
+ creator_loop.Run();
+
+ // The callback is called with nullptr profile.
+ EXPECT_TRUE(creator_callback_called());
+ EXPECT_FALSE(signed_in_profile());
+}
diff --git a/chromium/chrome/browser/signin/dice_tab_helper.cc b/chromium/chrome/browser/signin/dice_tab_helper.cc
new file mode 100644
index 00000000000..4e538d31a98
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_tab_helper.cc
@@ -0,0 +1,117 @@
+// Copyright 2017 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.
+
+#include "chrome/browser/signin/dice_tab_helper.h"
+
+#include "base/check_op.h"
+#include "base/metrics/user_metrics.h"
+#include "chrome/browser/signin/dice_tab_helper.h"
+#include "chrome/browser/signin/signin_util.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "content/public/browser/navigation_controller.h"
+#include "content/public/browser/navigation_entry.h"
+#include "content/public/browser/navigation_handle.h"
+#include "google_apis/gaia/gaia_urls.h"
+
+DiceTabHelper::DiceTabHelper(content::WebContents* web_contents)
+ : content::WebContentsUserData<DiceTabHelper>(*web_contents),
+ content::WebContentsObserver(web_contents) {}
+
+DiceTabHelper::~DiceTabHelper() = default;
+
+void DiceTabHelper::InitializeSigninFlow(
+ const GURL& signin_url,
+ signin_metrics::AccessPoint access_point,
+ signin_metrics::Reason reason,
+ signin_metrics::PromoAction promo_action,
+ const GURL& redirect_url) {
+ DCHECK(signin_url.is_valid());
+ DCHECK(signin_url_.is_empty() || signin_url_ == signin_url);
+
+ signin_url_ = signin_url;
+ signin_access_point_ = access_point;
+ signin_reason_ = reason;
+ signin_promo_action_ = promo_action;
+ is_chrome_signin_page_ = true;
+ signin_page_load_recorded_ = false;
+ redirect_url_ = redirect_url;
+ sync_signin_flow_status_ = SyncSigninFlowStatus::kNotStarted;
+
+ if (reason == signin_metrics::Reason::kSigninPrimaryAccount) {
+ sync_signin_flow_status_ = SyncSigninFlowStatus::kStarted;
+ signin_metrics::LogSigninAccessPointStarted(access_point, promo_action);
+ signin_metrics::RecordSigninUserActionForAccessPoint(access_point,
+ promo_action);
+ base::RecordAction(base::UserMetricsAction("Signin_SigninPage_Loading"));
+ }
+}
+
+bool DiceTabHelper::IsChromeSigninPage() const {
+ return is_chrome_signin_page_;
+}
+
+bool DiceTabHelper::IsSyncSigninInProgress() const {
+ return sync_signin_flow_status_ == SyncSigninFlowStatus::kStarted;
+}
+
+void DiceTabHelper::OnSyncSigninFlowComplete() {
+ // The flow is complete, reset to initial state.
+ sync_signin_flow_status_ = SyncSigninFlowStatus::kNotStarted;
+}
+
+void DiceTabHelper::DidStartNavigation(
+ content::NavigationHandle* navigation_handle) {
+ if (!is_chrome_signin_page_)
+ return;
+
+ // Ignore internal navigations.
+ if (!navigation_handle->IsInPrimaryMainFrame() ||
+ navigation_handle->IsSameDocument()) {
+ return;
+ }
+
+ if (!IsSigninPageNavigation(navigation_handle)) {
+ // Navigating away from the signin page.
+ // Note that currently any indication of a navigation is enough to consider
+ // this tab unsuitable for re-use, even if the navigation does not end up
+ // committing.
+ is_chrome_signin_page_ = false;
+ }
+}
+
+void DiceTabHelper::DidFinishNavigation(
+ content::NavigationHandle* navigation_handle) {
+ if (!is_chrome_signin_page_)
+ return;
+
+ // Ignore internal navigations.
+ if (!navigation_handle->IsInPrimaryMainFrame() ||
+ navigation_handle->IsSameDocument()) {
+ return;
+ }
+
+ if (!IsSigninPageNavigation(navigation_handle)) {
+ // Navigating away from the signin page.
+ // Note that currently any indication of a navigation is enough to consider
+ // this tab unsuitable for re-use, even if the navigation does not end up
+ // committing.
+ is_chrome_signin_page_ = false;
+ return;
+ }
+
+ if (!signin_page_load_recorded_) {
+ signin_page_load_recorded_ = true;
+ base::RecordAction(base::UserMetricsAction("Signin_SigninPage_Shown"));
+ }
+}
+
+bool DiceTabHelper::IsSigninPageNavigation(
+ content::NavigationHandle* navigation_handle) const {
+ return !navigation_handle->IsErrorPage() &&
+ navigation_handle->GetRedirectChain()[0] == signin_url_ &&
+ navigation_handle->GetURL().DeprecatedGetOriginAsURL() ==
+ GaiaUrls::GetInstance()->gaia_url();
+}
+
+WEB_CONTENTS_USER_DATA_KEY_IMPL(DiceTabHelper);
diff --git a/chromium/chrome/browser/signin/dice_tab_helper.h b/chromium/chrome/browser/signin/dice_tab_helper.h
new file mode 100644
index 00000000000..2f6190a4f98
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_tab_helper.h
@@ -0,0 +1,97 @@
+// Copyright 2017 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_SIGNIN_DICE_TAB_HELPER_H_
+#define CHROME_BROWSER_SIGNIN_DICE_TAB_HELPER_H_
+
+#include "components/signin/public/base/signin_metrics.h"
+#include "content/public/browser/web_contents_observer.h"
+#include "content/public/browser/web_contents_user_data.h"
+
+namespace content {
+class NavigationHandle;
+}
+
+// Tab helper used for DICE to tag signin tabs. Signin tabs can be reused.
+class DiceTabHelper : public content::WebContentsUserData<DiceTabHelper>,
+ public content::WebContentsObserver {
+ public:
+ DiceTabHelper(const DiceTabHelper&) = delete;
+ DiceTabHelper& operator=(const DiceTabHelper&) = delete;
+
+ ~DiceTabHelper() override;
+
+ signin_metrics::AccessPoint signin_access_point() const {
+ return signin_access_point_;
+ }
+
+ signin_metrics::PromoAction signin_promo_action() const {
+ return signin_promo_action_;
+ }
+
+ signin_metrics::Reason signin_reason() const { return signin_reason_; }
+
+ const GURL& redirect_url() const { return redirect_url_; }
+
+ const GURL& signin_url() const { return signin_url_; }
+
+ // Initializes the DiceTabHelper for a new signin flow. Must be called once
+ // per signin flow happening in the tab, when the signin URL is being loaded.
+ void InitializeSigninFlow(const GURL& signin_url,
+ signin_metrics::AccessPoint access_point,
+ signin_metrics::Reason reason,
+ signin_metrics::PromoAction promo_action,
+ const GURL& redirect_url);
+
+ // Returns true if this the tab is a re-usable chrome sign-in page (the signin
+ // page is loading or loaded in the tab).
+ // Returns false if the user or the page has navigated away from |signin_url|.
+ bool IsChromeSigninPage() const;
+
+ // Returns true if a signin flow was initialized with the reason
+ // kSigninPrimaryAccount and is not yet complete.
+ // Note that there is not guarantee that the flow would ever finish, and in
+ // some rare cases it is possible that a "non-sync" signin happens while this
+ // is true (if the user aborts the flow and then re-uses the same tab for a
+ // normal web signin).
+ bool IsSyncSigninInProgress() const;
+
+ // Called to notify that the sync signin is complete.
+ void OnSyncSigninFlowComplete();
+
+ private:
+ friend class content::WebContentsUserData<DiceTabHelper>;
+ explicit DiceTabHelper(content::WebContents* web_contents);
+
+ // kStarted: a Sync signin flow was started and not completed.
+ // kNotStarted: there is no sync signin flow in progress.
+ enum class SyncSigninFlowStatus { kNotStarted, kStarted };
+
+ // content::WebContentsObserver:
+ void DidStartNavigation(
+ content::NavigationHandle* navigation_handle) override;
+ void DidFinishNavigation(
+ content::NavigationHandle* navigation_handle) override;
+
+ // Returns true if this is a navigation to the signin URL.
+ bool IsSigninPageNavigation(
+ content::NavigationHandle* navigation_handle) const;
+
+ GURL redirect_url_;
+ GURL signin_url_;
+ signin_metrics::AccessPoint signin_access_point_ =
+ signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN;
+ signin_metrics::PromoAction signin_promo_action_ =
+ signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO;
+ signin_metrics::Reason signin_reason_ =
+ signin_metrics::Reason::kUnknownReason;
+ bool is_chrome_signin_page_ = false;
+ bool signin_page_load_recorded_ = false;
+ SyncSigninFlowStatus sync_signin_flow_status_ =
+ SyncSigninFlowStatus::kNotStarted;
+
+ WEB_CONTENTS_USER_DATA_KEY_DECL();
+};
+
+#endif // CHROME_BROWSER_SIGNIN_DICE_TAB_HELPER_H_
diff --git a/chromium/chrome/browser/signin/dice_tab_helper_unittest.cc b/chromium/chrome/browser/signin/dice_tab_helper_unittest.cc
new file mode 100644
index 00000000000..96eab075f2a
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_tab_helper_unittest.cc
@@ -0,0 +1,253 @@
+// Copyright 2017 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.
+
+#include "chrome/browser/signin/dice_tab_helper.h"
+
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/metrics/user_action_tester.h"
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "content/public/common/content_features.h"
+#include "content/public/test/back_forward_cache_util.h"
+#include "content/public/test/navigation_simulator.h"
+#include "content/public/test/test_renderer_host.h"
+#include "content/public/test/web_contents_tester.h"
+#include "google_apis/gaia/gaia_urls.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/features.h"
+
+class DiceTabHelperTest : public ChromeRenderViewHostTestHarness {
+ public:
+ DiceTabHelperTest() {
+ signin_url_ = GaiaUrls::GetInstance()->signin_chrome_sync_dice();
+ feature_list_.InitWithFeaturesAndParameters(
+ {{features::kBackForwardCache, {}},
+ {features::kBackForwardCacheMemoryControls, {}}},
+ {});
+ }
+
+ // Does a navigation to Gaia and initializes the tab helper.
+ void InitializeDiceTabHelper(DiceTabHelper* helper,
+ signin_metrics::AccessPoint access_point,
+ signin_metrics::Reason reason) {
+ // Load the signin page.
+ std::unique_ptr<content::NavigationSimulator> simulator =
+ content::NavigationSimulator::CreateRendererInitiated(signin_url_,
+ main_rfh());
+ simulator->Start();
+ helper->InitializeSigninFlow(
+ signin_url_, access_point, reason,
+ signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO,
+ GURL::EmptyGURL());
+ EXPECT_TRUE(helper->IsChromeSigninPage());
+ simulator->Commit();
+ }
+
+ GURL signin_url_;
+ base::test::ScopedFeatureList feature_list_;
+};
+
+// Tests DiceTabHelper intialization.
+TEST_F(DiceTabHelperTest, Initialization) {
+ DiceTabHelper::CreateForWebContents(web_contents());
+ DiceTabHelper* dice_tab_helper =
+ DiceTabHelper::FromWebContents(web_contents());
+
+ // Check default state.
+ EXPECT_EQ(signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN,
+ dice_tab_helper->signin_access_point());
+ EXPECT_EQ(signin_metrics::Reason::kUnknownReason,
+ dice_tab_helper->signin_reason());
+ EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage());
+
+ // Initialize the signin flow.
+ signin_metrics::AccessPoint access_point =
+ signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE;
+ signin_metrics::Reason reason = signin_metrics::Reason::kSigninPrimaryAccount;
+ InitializeDiceTabHelper(dice_tab_helper, access_point, reason);
+ EXPECT_EQ(access_point, dice_tab_helper->signin_access_point());
+ EXPECT_EQ(reason, dice_tab_helper->signin_reason());
+ EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage());
+}
+
+TEST_F(DiceTabHelperTest, SigninPageStatus) {
+ // The test assumes the previous page gets deleted after navigation and will
+ // be recreated after navigation (which resets the signin page state). Disable
+ // back/forward cache to ensure that it doesn't get preserved in the cache.
+ content::DisableBackForwardCacheForTesting(
+ web_contents(), content::BackForwardCache::TEST_ASSUMES_NO_CACHING);
+ DiceTabHelper::CreateForWebContents(web_contents());
+ DiceTabHelper* dice_tab_helper =
+ DiceTabHelper::FromWebContents(web_contents());
+ EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage());
+
+ // Load the signin page.
+ signin_metrics::AccessPoint access_point =
+ signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE;
+ signin_metrics::Reason reason = signin_metrics::Reason::kSigninPrimaryAccount;
+ InitializeDiceTabHelper(dice_tab_helper, access_point, reason);
+ EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage());
+
+ // Reloading the signin page does not interrupt the signin flow.
+ content::NavigationSimulator::Reload(web_contents());
+ EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage());
+
+ // Subframe navigation are ignored.
+ std::unique_ptr<content::NavigationSimulator> simulator =
+ content::NavigationSimulator::CreateRendererInitiated(
+ signin_url_.Resolve("#baz"), main_rfh());
+ simulator->CommitSameDocument();
+ EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage());
+
+ // Navigation in subframe does not interrupt the signin flow.
+ content::RenderFrameHostTester* render_frame_host_tester =
+ content::RenderFrameHostTester::For(main_rfh());
+ content::RenderFrameHost* sub_frame =
+ render_frame_host_tester->AppendChild("subframe");
+ content::NavigationSimulator::NavigateAndCommitFromDocument(signin_url_,
+ sub_frame);
+ EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage());
+
+ // Navigating to a different page resets the page status.
+ simulator = content::NavigationSimulator::CreateRendererInitiated(
+ signin_url_.Resolve("/foo"), main_rfh());
+ simulator->Start();
+ EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage());
+ simulator->Commit();
+ EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage());
+
+ // Go Back to the signin page
+ content::NavigationSimulator::GoBack(web_contents());
+ // IsChromeSigninPage() returns false after navigating away from the
+ // signin page.
+ EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage());
+
+ // Navigate away from the signin page
+ content::NavigationSimulator::GoForward(web_contents());
+ EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage());
+}
+
+// Tests DiceTabHelper metrics.
+TEST_F(DiceTabHelperTest, Metrics) {
+ base::UserActionTester ua_tester;
+ base::HistogramTester h_tester;
+ DiceTabHelper::CreateForWebContents(web_contents());
+ DiceTabHelper* dice_tab_helper =
+ DiceTabHelper::FromWebContents(web_contents());
+
+ // No metrics are logged when the Dice tab helper is created.
+ EXPECT_EQ(0, ua_tester.GetActionCount("Signin_Signin_FromStartPage"));
+ EXPECT_EQ(0, ua_tester.GetActionCount("Signin_SigninPage_Loading"));
+ EXPECT_EQ(0, ua_tester.GetActionCount("Signin_SigninPage_Shown"));
+
+ // Check metrics logged when the Dice tab helper is initialized.
+ std::unique_ptr<content::NavigationSimulator> simulator =
+ content::NavigationSimulator::CreateRendererInitiated(signin_url_,
+ main_rfh());
+ simulator->Start();
+ dice_tab_helper->InitializeSigninFlow(
+ signin_url_, signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS,
+ signin_metrics::Reason::kSigninPrimaryAccount,
+ signin_metrics::PromoAction::PROMO_ACTION_NEW_ACCOUNT_NO_EXISTING_ACCOUNT,
+ GURL::EmptyGURL());
+ EXPECT_EQ(1, ua_tester.GetActionCount("Signin_Signin_FromSettings"));
+ EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Loading"));
+ EXPECT_EQ(0, ua_tester.GetActionCount("Signin_SigninPage_Shown"));
+ h_tester.ExpectUniqueSample(
+ "Signin.SigninStartedAccessPoint",
+ signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, 1);
+ h_tester.ExpectUniqueSample(
+ "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount",
+ signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, 1);
+
+ // First call to did finish load does logs any Signin_SigninPage_Shown user
+ // action.
+ simulator->Commit();
+ EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Loading"));
+ EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Shown"));
+
+ // Second call to did finish load does not log any metrics.
+ dice_tab_helper->DidFinishLoad(main_rfh(), signin_url_);
+ EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Loading"));
+ EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Shown"));
+
+ // Check metrics are logged again when the Dice tab helper is re-initialized.
+ simulator = content::NavigationSimulator::CreateRendererInitiated(signin_url_,
+ main_rfh());
+ simulator->Start();
+ dice_tab_helper->InitializeSigninFlow(
+ signin_url_, signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS,
+ signin_metrics::Reason::kSigninPrimaryAccount,
+ signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT,
+ GURL::EmptyGURL());
+ EXPECT_EQ(2, ua_tester.GetActionCount("Signin_Signin_FromSettings"));
+ EXPECT_EQ(2, ua_tester.GetActionCount("Signin_SigninPage_Loading"));
+ h_tester.ExpectUniqueSample(
+ "Signin.SigninStartedAccessPoint",
+ signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, 2);
+ h_tester.ExpectUniqueSample(
+ "Signin.SigninStartedAccessPoint.WithDefault",
+ signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, 1);
+}
+
+TEST_F(DiceTabHelperTest, IsSyncSigninInProgress) {
+ DiceTabHelper::CreateForWebContents(web_contents());
+ DiceTabHelper* dice_tab_helper =
+ DiceTabHelper::FromWebContents(web_contents());
+ EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress());
+
+ // Non-sync signin.
+ InitializeDiceTabHelper(dice_tab_helper,
+ signin_metrics::AccessPoint::ACCESS_POINT_EXTENSIONS,
+ signin_metrics::Reason::kAddSecondaryAccount);
+ EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress());
+
+ // Sync signin
+ InitializeDiceTabHelper(dice_tab_helper,
+ signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS,
+ signin_metrics::Reason::kSigninPrimaryAccount);
+ EXPECT_TRUE(dice_tab_helper->IsSyncSigninInProgress());
+ dice_tab_helper->OnSyncSigninFlowComplete();
+ EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress());
+}
+
+class DiceTabHelperPrerenderTest : public DiceTabHelperTest {
+ public:
+ DiceTabHelperPrerenderTest() {
+ feature_list_.InitWithFeatures(
+ {blink::features::kPrerender2},
+ // Disable the memory requirement of Prerender2 so the test can run on
+ // any bot.
+ {blink::features::kPrerender2MemoryControls});
+ }
+
+ ~DiceTabHelperPrerenderTest() override = default;
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+};
+
+TEST_F(DiceTabHelperPrerenderTest, SigninStatusAfterPrerendering) {
+ base::UserActionTester ua_tester;
+ DiceTabHelper::CreateForWebContents(web_contents());
+ DiceTabHelper* dice_tab_helper =
+ DiceTabHelper::FromWebContents(web_contents());
+ EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage());
+ EXPECT_EQ(0, ua_tester.GetActionCount("Signin_SigninPage_Shown"));
+
+ // Sync signin
+ InitializeDiceTabHelper(dice_tab_helper,
+ signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS,
+ signin_metrics::Reason::kSigninPrimaryAccount);
+ dice_tab_helper->OnSyncSigninFlowComplete();
+ EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage());
+ EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Shown"));
+
+ // Starting prerendering a page doesn't navigate away from the signin page.
+ content::WebContentsTester::For(web_contents())
+ ->AddPrerenderAndCommitNavigation(signin_url_.Resolve("/foo/test.html"));
+ EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage());
+ EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Shown"));
+}
diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor.cc b/chromium/chrome/browser/signin/dice_web_signin_interceptor.cc
new file mode 100644
index 00000000000..751c3a58fad
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor.cc
@@ -0,0 +1,851 @@
+// Copyright 2020 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.
+
+#include "chrome/browser/signin/dice_web_signin_interceptor.h"
+
+#include <string>
+
+#include "base/check.h"
+#include "base/hash/hash.h"
+#include "base/i18n/case_conversion.h"
+#include "base/metrics/field_trial_params.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/strings/stringprintf.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/time/time.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/enterprise/util/managed_browser_utils.h"
+#include "chrome/browser/net/system_network_context_manager.h"
+#include "chrome/browser/new_tab_page/chrome_colors/generated_colors_info.h"
+#include "chrome/browser/password_manager/chrome_password_manager_client.h"
+#include "chrome/browser/policy/chrome_browser_policy_connector.h"
+#include "chrome/browser/policy/profile_policy_connector.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_avatar_icon_util.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/profiles/profile_metrics.h"
+#include "chrome/browser/profiles/profiles_state.h"
+#include "chrome/browser/signin/dice_intercepted_session_startup_helper.h"
+#include "chrome/browser/signin/dice_signed_in_profile_creator.h"
+#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "chrome/browser/signin/signin_util.h"
+#include "chrome/browser/themes/theme_service.h"
+#include "chrome/browser/themes/theme_service_factory.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/browser/ui/browser_window.h"
+#include "chrome/browser/ui/passwords/manage_passwords_ui_controller.h"
+#include "chrome/browser/ui/signin/profile_colors_util.h"
+#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h"
+#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper_delegate_impl.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/common/themes/autogenerated_theme_util.h"
+#include "components/password_manager/core/browser/password_manager.h"
+#include "components/password_manager/core/common/password_manager_ui.h"
+#include "components/policy/core/browser/browser_policy_connector.h"
+#include "components/policy/core/browser/signin/user_cloud_signin_restriction_policy_fetcher.h"
+#include "components/policy/core/common/features.h"
+#include "components/policy/core/common/policy_map.h"
+#include "components/policy/core/common/policy_namespace.h"
+#include "components/policy/core/common/policy_service.h"
+#include "components/policy/policy_constants.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/prefs/pref_service.h"
+#include "components/prefs/scoped_user_pref_update.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "components/signin/public/identity_manager/accounts_mutator.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "ui/base/l10n/l10n_util.h"
+
+namespace {
+
+constexpr char kProfileCreationInterceptionDeclinedPref[] =
+ "signin.ProfileCreationInterceptionDeclinedPref";
+
+void RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome outcome) {
+ base::UmaHistogramEnumeration("Signin.Intercept.HeuristicOutcome", outcome);
+}
+
+// Helper function to return the primary account info. The returned info is
+// empty if there is no primary account, and non-empty otherwise. Extended
+// fields may be missing if they are not available.
+AccountInfo GetPrimaryAccountInfo(signin::IdentityManager* manager) {
+ CoreAccountInfo primary_core_account_info =
+ manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
+ if (primary_core_account_info.IsEmpty())
+ return AccountInfo();
+
+ AccountInfo primary_account_info =
+ manager->FindExtendedAccountInfo(primary_core_account_info);
+
+ if (!primary_account_info.IsEmpty())
+ return primary_account_info;
+
+ // Return an AccountInfo without extended fields, based on the core info.
+ AccountInfo account_info;
+ account_info.gaia = primary_core_account_info.gaia;
+ account_info.email = primary_core_account_info.email;
+ account_info.account_id = primary_core_account_info.account_id;
+ return account_info;
+}
+
+bool HasNoBrowser(content::WebContents* web_contents) {
+ return chrome::FindBrowserWithWebContents(web_contents) == nullptr;
+}
+
+// Returns true if enterprise separation is required.
+// Returns false is enterprise separation is not required.
+// Returns no value if info is required to determine if enterprise separation is
+// required.
+// If `managed_account_profile_level_signin_restriction` is `absl::nullopt` then
+// the user cloud policy value of ManagedAccountsSigninRestriction has not yet
+// been fetched. If it is an empty string, then the value has been fetched but
+// no policy was set.
+absl::optional<bool> EnterpriseSeparationMaybeRequired(
+ Profile* profile,
+ const std::string& email,
+ signin::IdentityManager* identity_manager,
+ bool is_new_account_interception,
+ absl::optional<std::string>
+ managed_account_profile_level_signin_restriction) {
+ // No enterprise separation required if the feature is disabled.
+ if (!base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync))
+ return false;
+ // No enterprise separation required for consumer accounts.
+ if (policy::BrowserPolicyConnector::IsNonEnterpriseUser(email))
+ return false;
+
+ auto intercepted_account_info =
+ identity_manager->FindExtendedAccountInfoByEmailAddress(email);
+ // If the account info is not found, we need to wait for the info to be
+ // available.
+ if (!intercepted_account_info.IsValid())
+ return absl::nullopt;
+ // If the intercepted account is not managed, no interception required.
+ if (!intercepted_account_info.IsManaged())
+ return false;
+ // If `profile` requires enterprise profile separation, return true.
+ if (signin_util::ProfileSeparationEnforcedByPolicy(
+ profile, managed_account_profile_level_signin_restriction.value_or(
+ std::string()))) {
+ return true;
+ }
+ // If we still do not know if profile separation is required, the account
+ // level policies for the intercepted account must be fetched if possible.
+ if (is_new_account_interception &&
+ base::FeatureList::IsEnabled(
+ policy::features::kEnableUserCloudSigninRestrictionPolicyFetcher) &&
+ !managed_account_profile_level_signin_restriction.has_value() &&
+ g_browser_process->system_network_context_manager()) {
+ return absl::nullopt;
+ }
+
+ return false;
+}
+
+} // namespace
+
+ScopedDiceWebSigninInterceptionBubbleHandle::
+ ~ScopedDiceWebSigninInterceptionBubbleHandle() = default;
+
+bool SigninInterceptionHeuristicOutcomeIsSuccess(
+ SigninInterceptionHeuristicOutcome outcome) {
+ return outcome == SigninInterceptionHeuristicOutcome::kInterceptEnterprise ||
+ outcome == SigninInterceptionHeuristicOutcome::kInterceptMultiUser ||
+ outcome == SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch;
+}
+
+DiceWebSigninInterceptor::DiceWebSigninInterceptor(
+ Profile* profile,
+ std::unique_ptr<Delegate> delegate)
+ : profile_(profile),
+ identity_manager_(IdentityManagerFactory::GetForProfile(profile)),
+ delegate_(std::move(delegate)) {
+ DCHECK(profile_);
+ DCHECK(identity_manager_);
+ DCHECK(delegate_);
+}
+
+DiceWebSigninInterceptor::~DiceWebSigninInterceptor() = default;
+
+// static
+void DiceWebSigninInterceptor::RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) {
+ registry->RegisterDictionaryPref(kProfileCreationInterceptionDeclinedPref);
+ registry->RegisterBooleanPref(prefs::kSigninInterceptionEnabled, true);
+ registry->RegisterStringPref(prefs::kManagedAccountsSigninRestriction,
+ std::string());
+ registry->RegisterBooleanPref(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine, false);
+}
+
+absl::optional<SigninInterceptionHeuristicOutcome>
+DiceWebSigninInterceptor::GetHeuristicOutcome(
+ bool is_new_account,
+ bool is_sync_signin,
+ const std::string& email,
+ const ProfileAttributesEntry** entry) const {
+ if (!profile_->GetPrefs()->GetBoolean(prefs::kSigninInterceptionEnabled))
+ return SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled;
+
+ if (is_sync_signin) {
+ // Do not intercept signins from the Sync startup flow.
+ // Note: |is_sync_signin| is an approximation, and in rare cases it may be
+ // true when in fact the signin was not a sync signin. In this case the
+ // interception is missed.
+ return SigninInterceptionHeuristicOutcome::kAbortSyncSignin;
+ }
+ // Wait for more account info is enterprise separation is required or if more
+ // info is needed.
+ if (EnterpriseSeparationMaybeRequired(
+ profile_, email, identity_manager_, is_new_account,
+ /*managed_account_profile_level_signin_restriction=*/absl::nullopt)
+ .value_or(true)) {
+ return absl::nullopt;
+ }
+
+ if (!is_new_account) {
+ // Do not intercept reauth.
+ return SigninInterceptionHeuristicOutcome::kAbortAccountNotNew;
+ }
+
+ const ProfileAttributesEntry* switch_to_entry = ShouldShowProfileSwitchBubble(
+ email,
+ &g_browser_process->profile_manager()->GetProfileAttributesStorage());
+ if (switch_to_entry) {
+ if (entry)
+ *entry = switch_to_entry;
+ return SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch;
+ }
+
+ // From this point the remaining possible interceptions involve creating a new
+ // profile.
+ if (!profiles::IsProfileCreationAllowed()) {
+ return SigninInterceptionHeuristicOutcome::kAbortProfileCreationDisallowed;
+ }
+
+ std::vector<CoreAccountInfo> accounts_in_chrome =
+ identity_manager_->GetAccountsWithRefreshTokens();
+ if (accounts_in_chrome.size() == 0 ||
+ (accounts_in_chrome.size() == 1 &&
+ gaia::AreEmailsSame(email, accounts_in_chrome[0].email))) {
+ // Enterprise and multi-user bubbles are only shown if there are multiple
+ // accounts. The intercepted account may not be added to chrome yet.
+ return SigninInterceptionHeuristicOutcome::kAbortSingleAccount;
+ }
+
+ if (HasUserDeclinedProfileCreation(email)) {
+ return SigninInterceptionHeuristicOutcome::
+ kAbortUserDeclinedProfileForAccount;
+ }
+
+ return absl::nullopt;
+}
+
+void DiceWebSigninInterceptor::MaybeInterceptWebSignin(
+ content::WebContents* web_contents,
+ CoreAccountId account_id,
+ bool is_new_account,
+ bool is_sync_signin) {
+ if (is_interception_in_progress_) {
+ // Multiple concurrent interceptions are not supported.
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kAbortInterceptInProgress);
+ return;
+ }
+
+ if (!web_contents) {
+ // The tab has been closed (typically during the token exchange, which may
+ // take some time).
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kAbortTabClosed);
+ return;
+ }
+
+ if (HasNoBrowser(web_contents)) {
+ // Do not intercept from the profile creation flow.
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kAbortNoBrowser);
+ return;
+ }
+
+ // Do not show the interception UI if a password update is required: both
+ // bubbles cannot be shown at the same time and the password update is more
+ // important.
+ ChromePasswordManagerClient* password_manager_client =
+ ChromePasswordManagerClient::FromWebContents(web_contents);
+ if (password_manager_client && password_manager_client->GetPasswordManager()
+ ->IsFormManagerPendingPasswordUpdate()) {
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kAbortPasswordUpdatePending);
+ return;
+ }
+
+ ManagePasswordsUIController* password_controller =
+ ManagePasswordsUIController::FromWebContents(web_contents);
+ if (password_controller &&
+ password_controller->GetState() ==
+ password_manager::ui::State::PENDING_PASSWORD_UPDATE_STATE) {
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kAbortPasswordUpdate);
+ return;
+ }
+
+ AccountInfo account_info =
+ identity_manager_->FindExtendedAccountInfoByAccountId(account_id);
+ DCHECK(!account_info.IsEmpty()) << "Intercepting unknown account.";
+ const ProfileAttributesEntry* entry = nullptr;
+ absl::optional<SigninInterceptionHeuristicOutcome> heuristic_outcome =
+ GetHeuristicOutcome(is_new_account, is_sync_signin, account_info.email,
+ &entry);
+ account_id_ = account_id;
+ is_interception_in_progress_ = true;
+ new_account_interception_ = is_new_account;
+ web_contents_ = web_contents->GetWeakPtr();
+
+ if (heuristic_outcome) {
+ RecordSigninInterceptionHeuristicOutcome(*heuristic_outcome);
+ if (*heuristic_outcome ==
+ SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch) {
+ DCHECK(entry);
+ Delegate::BubbleParameters bubble_parameters{
+ SigninInterceptionType::kProfileSwitch, account_info,
+ GetPrimaryAccountInfo(identity_manager_),
+ entry->GetProfileThemeColors().profile_highlight_color,
+ /*show_guest_option=*/false};
+ interception_bubble_handle_ = delegate_->ShowSigninInterceptionBubble(
+ web_contents, bubble_parameters,
+ base::BindOnce(&DiceWebSigninInterceptor::OnProfileSwitchChoice,
+ base::Unretained(this), account_info.email,
+ entry->GetPath()));
+ was_interception_ui_displayed_ = true;
+ } else {
+ // Interception is aborted.
+ DCHECK(!SigninInterceptionHeuristicOutcomeIsSuccess(*heuristic_outcome));
+ Reset();
+ }
+ return;
+ }
+
+ account_info_fetch_start_time_ = base::TimeTicks::Now();
+ if (account_info.IsValid()) {
+ OnExtendedAccountInfoUpdated(account_info);
+ } else {
+ on_account_info_update_timeout_.Reset(base::BindOnce(
+ &DiceWebSigninInterceptor::OnExtendedAccountInfoFetchTimeout,
+ base::Unretained(this)));
+ base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
+ FROM_HERE, on_account_info_update_timeout_.callback(),
+ base::Seconds(5));
+ account_info_update_observation_.Observe(identity_manager_.get());
+ }
+}
+
+void DiceWebSigninInterceptor::CreateBrowserAfterSigninInterception(
+ CoreAccountId account_id,
+ content::WebContents* intercepted_contents,
+ std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle> bubble_handle,
+ bool is_new_profile) {
+ DCHECK(!session_startup_helper_);
+ DCHECK(bubble_handle);
+ interception_bubble_handle_ = std::move(bubble_handle);
+ session_startup_helper_ =
+ std::make_unique<DiceInterceptedSessionStartupHelper>(
+ profile_, is_new_profile, account_id, intercepted_contents);
+ session_startup_helper_->Startup(
+ base::BindOnce(&DiceWebSigninInterceptor::OnNewBrowserCreated,
+ base::Unretained(this), is_new_profile));
+}
+
+void DiceWebSigninInterceptor::Shutdown() {
+ if (is_interception_in_progress_ && !was_interception_ui_displayed_) {
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kAbortShutdown);
+ }
+ Reset();
+}
+
+void DiceWebSigninInterceptor::Reset() {
+ web_contents_ = nullptr;
+ account_info_update_observation_.Reset();
+ on_account_info_update_timeout_.Cancel();
+ is_interception_in_progress_ = false;
+ account_id_ = CoreAccountId();
+ new_account_interception_ = false;
+ intercepted_account_management_accepted_ = false;
+ dice_signed_in_profile_creator_.reset();
+ was_interception_ui_displayed_ = false;
+ account_info_fetch_start_time_ = base::TimeTicks();
+ profile_creation_start_time_ = base::TimeTicks();
+ interception_bubble_handle_.reset();
+ on_intercepted_account_level_policy_value_timeout_.Cancel();
+ account_level_signin_restriction_policy_fetcher_.reset();
+ intercepted_account_level_policy_value_.reset();
+}
+
+const ProfileAttributesEntry*
+DiceWebSigninInterceptor::ShouldShowProfileSwitchBubble(
+ const std::string& intercepted_email,
+ ProfileAttributesStorage* profile_attribute_storage) const {
+ // Check if there is already an existing profile with this account.
+ base::FilePath profile_path = profile_->GetPath();
+ for (const auto* entry :
+ profile_attribute_storage->GetAllProfilesAttributes()) {
+ if (entry->GetPath() == profile_path)
+ continue;
+ if (gaia::AreEmailsSame(intercepted_email,
+ base::UTF16ToUTF8(entry->GetUserName()))) {
+ return entry;
+ }
+ }
+ return nullptr;
+}
+
+bool DiceWebSigninInterceptor::ShouldEnforceEnterpriseProfileSeparation(
+ const AccountInfo& intercepted_account_info) const {
+ DCHECK(intercepted_account_info.IsValid());
+
+ if (!signin_util::ProfileSeparationEnforcedByPolicy(
+ profile_,
+ intercepted_account_level_policy_value_.value_or(std::string()))) {
+ return false;
+ }
+ if (new_account_interception_)
+ return intercepted_account_info.IsManaged();
+
+ CoreAccountInfo primary_core_account_info =
+ identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
+ // In case of re-auth, do not show the enterprise separation dialog if the
+ // user already consented to enterprise management.
+ if (!new_account_interception_ && primary_core_account_info.account_id ==
+ intercepted_account_info.account_id) {
+ return !chrome::enterprise_util::UserAcceptedAccountManagement(profile_);
+ }
+
+ return false;
+}
+
+bool DiceWebSigninInterceptor::ShouldShowEnterpriseBubble(
+ const AccountInfo& intercepted_account_info) const {
+ DCHECK(intercepted_account_info.IsValid());
+ // Check if the intercepted account or the primary account is managed.
+ CoreAccountInfo primary_core_account_info =
+ identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
+
+ if (primary_core_account_info.IsEmpty() ||
+ primary_core_account_info.account_id ==
+ intercepted_account_info.account_id) {
+ return false;
+ }
+
+ if (intercepted_account_info.IsManaged())
+ return true;
+
+ return identity_manager_->FindExtendedAccountInfo(primary_core_account_info)
+ .IsManaged();
+}
+
+bool DiceWebSigninInterceptor::ShouldShowMultiUserBubble(
+ const AccountInfo& intercepted_account_info) const {
+ DCHECK(intercepted_account_info.IsValid());
+ if (identity_manager_->GetAccountsWithRefreshTokens().size() <= 1u)
+ return false;
+ // Check if the account has the same name as another account in the profile.
+ for (const auto& account_info :
+ identity_manager_->GetExtendedAccountInfoForAccountsWithRefreshToken()) {
+ if (account_info.account_id == intercepted_account_info.account_id)
+ continue;
+ // Case-insensitve comparison supporting non-ASCII characters.
+ if (base::i18n::FoldCase(base::UTF8ToUTF16(account_info.given_name)) ==
+ base::i18n::FoldCase(
+ base::UTF8ToUTF16(intercepted_account_info.given_name))) {
+ return false;
+ }
+ }
+ return true;
+}
+
+void DiceWebSigninInterceptor::OnInterceptionReadyToBeProcessed(
+ const AccountInfo& info) {
+ DCHECK_EQ(info.account_id, account_id_);
+ DCHECK(info.IsValid());
+
+ absl::optional<SigninInterceptionType> interception_type;
+
+ ProfileAttributesEntry* entry =
+ g_browser_process->profile_manager()
+ ->GetProfileAttributesStorage()
+ .GetProfileAttributesWithPath(profile_->GetPath());
+ SkColor profile_color = GenerateNewProfileColor(entry).color;
+
+ const ProfileAttributesEntry* switch_to_entry = ShouldShowProfileSwitchBubble(
+ info.email,
+ &g_browser_process->profile_manager()->GetProfileAttributesStorage());
+
+ bool force_profile_separation =
+ ShouldEnforceEnterpriseProfileSeparation(info);
+
+#if DCHECK_IS_ON()
+ if (force_profile_separation) {
+ DCHECK(base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync) ||
+ !profile_->GetPrefs()
+ ->GetString(prefs::kManagedAccountsSigninRestriction)
+ .empty());
+ }
+#endif
+
+ if (switch_to_entry) {
+ // Propose account switching if we skipped in GetHeuristicOutcome because we
+ // returned a nullptr to get more information about forced enterprise
+ // profile separation.
+ interception_type = force_profile_separation
+ ? SigninInterceptionType::kProfileSwitchForced
+ : SigninInterceptionType::kProfileSwitch;
+ RecordSigninInterceptionHeuristicOutcome(
+ force_profile_separation
+ ? SigninInterceptionHeuristicOutcome::
+ kInterceptEnterpriseForcedProfileSwitch
+ : SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch);
+ } else if (force_profile_separation) {
+ // In case of a reauth of an account that already had sync enabled,
+ // the user already accepted to use a managed profile. Simply update that
+ // fact.
+ if (!new_account_interception_ &&
+ identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSync) ==
+ info.account_id) {
+ chrome::enterprise_util::SetUserAcceptedAccountManagement(profile_, true);
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kAbortAccountNotNew);
+ Reset();
+ return;
+ }
+ interception_type = SigninInterceptionType::kEnterpriseForced;
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced);
+ } else if (ShouldShowEnterpriseBubble(info)) {
+ interception_type = SigninInterceptionType::kEnterprise;
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kInterceptEnterprise);
+ } else if (ShouldShowMultiUserBubble(info)) {
+ interception_type = SigninInterceptionType::kMultiUser;
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kInterceptMultiUser);
+ }
+
+ if (!interception_type) {
+ // Signin should not be intercepted.
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kAbortAccountInfoNotCompatible);
+ Reset();
+ return;
+ }
+
+ Delegate::BubbleParameters bubble_parameters{
+ *interception_type, info, GetPrimaryAccountInfo(identity_manager_),
+ GetAutogeneratedThemeColors(profile_color).frame_color,
+ /*show_guest_option=*/false};
+
+ base::OnceCallback<void(SigninInterceptionResult)> callback;
+ switch (*interception_type) {
+ case SigninInterceptionType::kProfileSwitchForced:
+ callback = base::BindOnce(
+ &DiceWebSigninInterceptor::OnProfileSwitchChoice,
+ base::Unretained(this), info.email, switch_to_entry->GetPath());
+ break;
+ case SigninInterceptionType::kEnterpriseForced:
+ callback = base::BindOnce(
+ &DiceWebSigninInterceptor::OnEnterpriseProfileCreationResult,
+ base::Unretained(this), info, profile_color);
+ break;
+ case SigninInterceptionType::kProfileSwitch:
+ case SigninInterceptionType::kEnterprise:
+ case SigninInterceptionType::kMultiUser:
+ callback =
+ base::BindOnce(&DiceWebSigninInterceptor::OnProfileCreationChoice,
+ base::Unretained(this), info, profile_color);
+ break;
+ }
+ interception_bubble_handle_ = delegate_->ShowSigninInterceptionBubble(
+ web_contents_.get(), bubble_parameters, std::move(callback));
+
+ was_interception_ui_displayed_ = true;
+}
+
+void DiceWebSigninInterceptor::OnExtendedAccountInfoUpdated(
+ const AccountInfo& info) {
+ if (info.account_id != account_id_)
+ return;
+ if (!info.IsValid())
+ return;
+
+ account_info_update_observation_.Reset();
+ on_account_info_update_timeout_.Cancel();
+ base::UmaHistogramTimes(
+ "Signin.Intercept.AccountInfoFetchDuration",
+ base::TimeTicks::Now() - account_info_fetch_start_time_);
+
+ // Fetch the ManagedAccountsSigninRestriction policy value for the intercepted
+ // account with a timeout.
+ if (!EnterpriseSeparationMaybeRequired(
+ profile_, info.email, identity_manager_, new_account_interception_,
+ intercepted_account_level_policy_value_)
+ .has_value()) {
+ FetchAccountLevelSigninRestrictionForInterceptedAccount(
+ info, base::BindOnce(
+ &DiceWebSigninInterceptor::
+ OnAccountLevelManagedAccountsSigninRestrictionReceived,
+ base::Unretained(this), /*timed_out=*/false, info));
+ return;
+ }
+
+ OnInterceptionReadyToBeProcessed(info);
+}
+
+void DiceWebSigninInterceptor::OnExtendedAccountInfoFetchTimeout() {
+ RecordSigninInterceptionHeuristicOutcome(
+ SigninInterceptionHeuristicOutcome::kAbortAccountInfoTimeout);
+ Reset();
+}
+
+void DiceWebSigninInterceptor::OnProfileCreationChoice(
+ const AccountInfo& account_info,
+ SkColor profile_color,
+ SigninInterceptionResult create) {
+ if (create != SigninInterceptionResult::kAccepted &&
+ create != SigninInterceptionResult::kAcceptedWithGuest) {
+ if (create == SigninInterceptionResult::kDeclined)
+ RecordProfileCreationDeclined(account_info.email);
+ Reset();
+ return;
+ }
+
+ DCHECK(interception_bubble_handle_);
+ profile_creation_start_time_ = base::TimeTicks::Now();
+ std::u16string profile_name;
+ profile_name = profiles::GetDefaultNameForNewSignedInProfile(account_info);
+
+ DCHECK(!dice_signed_in_profile_creator_);
+ // Unretained is fine because the profile creator is owned by this.
+ dice_signed_in_profile_creator_ =
+ std::make_unique<DiceSignedInProfileCreator>(
+ profile_, account_id_, profile_name,
+ profiles::GetPlaceholderAvatarIndex(),
+ create == SigninInterceptionResult::kAcceptedWithGuest,
+ base::BindOnce(&DiceWebSigninInterceptor::OnNewSignedInProfileCreated,
+ base::Unretained(this), profile_color));
+}
+
+void DiceWebSigninInterceptor::OnProfileSwitchChoice(
+ const std::string& email,
+ const base::FilePath& profile_path,
+ SigninInterceptionResult switch_profile) {
+ if (switch_profile != SigninInterceptionResult::kAccepted) {
+ Reset();
+ return;
+ }
+
+ DCHECK(interception_bubble_handle_);
+ DCHECK(!dice_signed_in_profile_creator_);
+ profile_creation_start_time_ = base::TimeTicks::Now();
+ // Unretained is fine because the profile creator is owned by this.
+ dice_signed_in_profile_creator_ =
+ std::make_unique<DiceSignedInProfileCreator>(
+ profile_, account_id_, profile_path,
+ base::BindOnce(&DiceWebSigninInterceptor::OnNewSignedInProfileCreated,
+ base::Unretained(this), absl::nullopt));
+}
+
+void DiceWebSigninInterceptor::OnNewSignedInProfileCreated(
+ absl::optional<SkColor> profile_color,
+ Profile* new_profile) {
+ DCHECK(dice_signed_in_profile_creator_);
+ dice_signed_in_profile_creator_.reset();
+
+ if (!new_profile) {
+ Reset();
+ return;
+ }
+
+ // The profile color is defined only when the profile has just been created
+ // (with interception type kMultiUser or kEnterprise). If the profile is not
+ // new (kProfileSwitch) or if it is a guest profile, then the color is not
+ // updated.
+ bool is_new_profile = profile_color.has_value();
+ if (is_new_profile) {
+ base::UmaHistogramTimes(
+ "Signin.Intercept.ProfileCreationDuration",
+ base::TimeTicks::Now() - profile_creation_start_time_);
+ ProfileMetrics::LogProfileAddNewUser(
+ ProfileMetrics::ADD_NEW_USER_SIGNIN_INTERCEPTION);
+ // TODO(https://crbug.com/1225171): Remove the condition if Guest mode
+ // option is removed.
+ if (!new_profile->IsGuestSession()) {
+ // Apply the new color to the profile.
+ ThemeServiceFactory::GetForProfile(new_profile)
+ ->BuildAutogeneratedThemeFromColor(*profile_color);
+ }
+ } else {
+ base::UmaHistogramTimes(
+ "Signin.Intercept.ProfileSwitchDuration",
+ base::TimeTicks::Now() - profile_creation_start_time_);
+ }
+
+ if (base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync)) {
+ chrome::enterprise_util::SetUserAcceptedAccountManagement(
+ new_profile, intercepted_account_management_accepted_);
+ }
+
+ // Work is done in this profile, the flow continues in the
+ // DiceWebSigninInterceptor that is attached to the new profile.
+ DiceWebSigninInterceptorFactory::GetForProfile(new_profile)
+ ->CreateBrowserAfterSigninInterception(
+ account_id_, web_contents_.get(),
+ std::move(interception_bubble_handle_), is_new_profile);
+ Reset();
+}
+
+void DiceWebSigninInterceptor::OnEnterpriseProfileCreationResult(
+ const AccountInfo& account_info,
+ SkColor profile_color,
+ SigninInterceptionResult create) {
+ DCHECK(base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync));
+ if (create == SigninInterceptionResult::kAccepted) {
+ intercepted_account_management_accepted_ = true;
+ // In case of a reauth if there was no consent for management, do not create
+ // a new profile.
+ if (!new_account_interception_ &&
+ GetPrimaryAccountInfo(identity_manager_).account_id ==
+ account_info.account_id) {
+ chrome::enterprise_util::SetUserAcceptedAccountManagement(
+ profile_, intercepted_account_management_accepted_);
+ Reset();
+ } else {
+ OnProfileCreationChoice(account_info, profile_color,
+ SigninInterceptionResult::kAccepted);
+ }
+ } else {
+ DCHECK_EQ(SigninInterceptionResult::kDeclined, create)
+ << "The user can only accept or decline";
+ OnProfileCreationChoice(account_info, profile_color,
+ SigninInterceptionResult::kDeclined);
+ auto* accounts_mutator = identity_manager_->GetAccountsMutator();
+ accounts_mutator->RemoveAccount(
+ account_info.account_id,
+ signin_metrics::SourceForRefreshTokenOperation::
+ kDiceTurnOnSyncHelper_Abort);
+ }
+ signin_util::RecordEnterpriseProfileCreationUserChoice(
+ /*enforced_by_policy=*/signin_util::ProfileSeparationEnforcedByPolicy(
+ profile_,
+ intercepted_account_level_policy_value_.value_or(std::string())),
+ /*created=*/create == SigninInterceptionResult::kAccepted);
+}
+
+void DiceWebSigninInterceptor::OnNewBrowserCreated(bool is_new_profile) {
+ DCHECK(interception_bubble_handle_);
+ interception_bubble_handle_.reset(); // Close the bubble now.
+ session_startup_helper_.reset();
+
+ // TODO(https://crbug.com/1225171): Remove |IsGuestSession| if Guest option is
+ // no more supported.
+ if (!is_new_profile || profile_->IsGuestSession())
+ return;
+
+ // Don't show the customization bubble if a valid policy theme is set.
+ Browser* browser = chrome::FindBrowserWithProfile(profile_);
+ if (ThemeServiceFactory::GetForProfile(profile_)->UsingPolicyTheme()) {
+ // Show the profile switch IPH that is normally shown after the
+ // customization bubble.
+ browser->window()->MaybeShowProfileSwitchIPH();
+ return;
+ }
+
+ DCHECK(browser);
+ delegate_->ShowProfileCustomizationBubble(browser);
+}
+
+// static
+std::string DiceWebSigninInterceptor::GetPersistentEmailHash(
+ const std::string& email) {
+ int hash = base::PersistentHash(
+ gaia::CanonicalizeEmail(gaia::SanitizeEmail(email))) &
+ 0xFF;
+ return base::StringPrintf("email_%i", hash);
+}
+
+void DiceWebSigninInterceptor::RecordProfileCreationDeclined(
+ const std::string& email) {
+ DictionaryPrefUpdate update(profile_->GetPrefs(),
+ kProfileCreationInterceptionDeclinedPref);
+ std::string key = GetPersistentEmailHash(email);
+ absl::optional<int> declined_count = update->FindIntKey(key);
+ update->SetIntKey(key, declined_count.value_or(0) + 1);
+}
+
+bool DiceWebSigninInterceptor::HasUserDeclinedProfileCreation(
+ const std::string& email) const {
+ const base::DictionaryValue* pref_data = profile_->GetPrefs()->GetDictionary(
+ kProfileCreationInterceptionDeclinedPref);
+ absl::optional<int> declined_count =
+ pref_data->FindIntKey(GetPersistentEmailHash(email));
+ // Check if the user declined 2 times.
+ constexpr int kMaxProfileCreationDeclinedCount = 2;
+ return declined_count &&
+ declined_count.value() >= kMaxProfileCreationDeclinedCount;
+}
+
+void DiceWebSigninInterceptor::
+ FetchAccountLevelSigninRestrictionForInterceptedAccount(
+ const AccountInfo& account_info,
+ base::OnceCallback<void(const std::string&)> callback) {
+ DCHECK(base::FeatureList::IsEnabled(
+ policy::features::kEnableUserCloudSigninRestrictionPolicyFetcher));
+ if (intercepted_account_level_policy_value_fetch_result_for_testing_
+ .has_value()) {
+ std::move(callback).Run(
+ intercepted_account_level_policy_value_fetch_result_for_testing_
+ .value());
+ return;
+ }
+
+ account_level_signin_restriction_policy_fetcher_ =
+ std::make_unique<policy::UserCloudSigninRestrictionPolicyFetcher>(
+ g_browser_process->browser_policy_connector(),
+ g_browser_process->system_network_context_manager()
+ ->GetSharedURLLoaderFactory());
+ account_level_signin_restriction_policy_fetcher_
+ ->GetManagedAccountsSigninRestriction(
+ identity_manager_, account_info.account_id, std::move(callback));
+
+ on_intercepted_account_level_policy_value_timeout_.Reset(base::BindOnce(
+ &DiceWebSigninInterceptor::
+ OnAccountLevelManagedAccountsSigninRestrictionReceived,
+ base::Unretained(this), /*timed_out=*/true, account_info, std::string()));
+ base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
+ FROM_HERE, on_intercepted_account_level_policy_value_timeout_.callback(),
+ base::Seconds(5));
+}
+
+void DiceWebSigninInterceptor::
+ OnAccountLevelManagedAccountsSigninRestrictionReceived(
+ bool timed_out,
+ const AccountInfo& account_info,
+ const std::string& signin_restriction) {
+#if DCHECK_IS_ON()
+ if (timed_out) {
+ DCHECK(signin_restriction.empty())
+ << "There should be no signin restriction at the account level in case "
+ "of a timeout";
+ }
+#endif
+ intercepted_account_level_policy_value_ = signin_restriction;
+ OnInterceptionReadyToBeProcessed(account_info);
+}
diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor.h b/chromium/chrome/browser/signin/dice_web_signin_interceptor.h
new file mode 100644
index 00000000000..0e3a03f41c3
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor.h
@@ -0,0 +1,411 @@
+// Copyright 2020 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_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_H_
+#define CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_H_
+
+#include <memory>
+
+#include "base/callback_forward.h"
+#include "base/cancelable_callback.h"
+#include "base/feature_list.h"
+#include "base/gtest_prod_util.h"
+#include "base/memory/raw_ptr.h"
+#include "base/scoped_observation.h"
+#include "base/time/time.h"
+#include "chrome/browser/ui/webui/signin/enterprise_profile_welcome_ui.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "google_apis/gaia/core_account_id.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/skia/include/core/SkColor.h"
+
+namespace base {
+class FilePath;
+}
+
+namespace content {
+class WebContents;
+}
+
+namespace policy {
+class UserCloudSigninRestrictionPolicyFetcher;
+}
+namespace user_prefs {
+class PrefRegistrySyncable;
+}
+
+struct AccountInfo;
+class Browser;
+class DiceSignedInProfileCreator;
+class DiceInterceptedSessionStartupHelper;
+class Profile;
+class ProfileAttributesEntry;
+class ProfileAttributesStorage;
+
+// Outcome of the interception heuristic (decision whether the interception
+// bubble is shown or not).
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class SigninInterceptionHeuristicOutcome {
+ // Interception succeeded:
+ kInterceptProfileSwitch = 0,
+ kInterceptMultiUser = 1,
+ kInterceptEnterprise = 2,
+
+ // Interception aborted:
+ // This is a "Sync" sign in and not a "web" sign in.
+ kAbortSyncSignin = 3,
+ // Another interception is already in progress.
+ kAbortInterceptInProgress = 4,
+ // This is not a new account (reauth).
+ kAbortAccountNotNew = 5,
+ // New profile is not offered when there is only one account.
+ kAbortSingleAccount = 6,
+ // Extended account info could not be downloaded.
+ kAbortAccountInfoTimeout = 7,
+ // Account info not compatible with interception (e.g. same Gaia name).
+ kAbortAccountInfoNotCompatible = 8,
+ // Profile creation disallowed.
+ kAbortProfileCreationDisallowed = 9,
+ // The interceptor was shut down before the heuristic completed.
+ kAbortShutdown = 10,
+ // The interceptor is not offered when WebContents has no browser associated.
+ kAbortNoBrowser = 11,
+ // A password update is required for the account, and this takes priority over
+ // signin interception.
+ kAbortPasswordUpdate = 12,
+ // A password update will be required for the account: the password used on
+ // the form does not match the stored password.
+ kAbortPasswordUpdatePending = 13,
+ // The user already declined a new profile for this account, the UI is not
+ // shown again.
+ kAbortUserDeclinedProfileForAccount = 14,
+ // Signin interception is disabled by the SigninInterceptionEnabled policy.
+ kAbortInterceptionDisabled = 15,
+
+ // Interception succeeded when enteprise account separation is mandatory.
+ kInterceptEnterpriseForced = 16,
+ kInterceptEnterpriseForcedProfileSwitch = 17,
+
+ // The interceptor is not triggered if the tab has already been closed.
+ kAbortTabClosed = 18,
+
+ kMaxValue = kAbortTabClosed,
+};
+
+// User selection in the interception bubble.
+enum class SigninInterceptionUserChoice { kAccept, kDecline, kGuest };
+
+// User action resulting from the interception bubble.
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class SigninInterceptionResult {
+ kAccepted = 0,
+ kDeclined = 1,
+ kIgnored = 2,
+
+ // Used when the bubble was not shown because it's not implemented.
+ kNotDisplayed = 3,
+
+ // Accepted to be opened in Guest profile.
+ kAcceptedWithGuest = 4,
+
+ kMaxValue = kAcceptedWithGuest,
+};
+
+// The ScopedDiceWebSigninInterceptionBubbleHandle closes the signin intercept
+// bubble when it is destroyed, if the bubble is still opened. Note that this
+// handle does not prevent the bubble from being closed for other reasons.
+class ScopedDiceWebSigninInterceptionBubbleHandle {
+ public:
+ virtual ~ScopedDiceWebSigninInterceptionBubbleHandle() = 0;
+};
+
+// Returns whether the heuristic outcome is a success (the signin should be
+// intercepted).
+bool SigninInterceptionHeuristicOutcomeIsSuccess(
+ SigninInterceptionHeuristicOutcome outcome);
+
+// Called after web signed in, after a successful token exchange through Dice.
+// The DiceWebSigninInterceptor may offer the user to create a new profile or
+// switch to another existing profile.
+//
+// Implementation notes: here is how an entire interception flow work for the
+// enterprise or multi-user case:
+// * MaybeInterceptWebSignin() is called when the new signin happens.
+// * Wait until the account info is downloaded.
+// * Interception UI is shown by the delegate. Keep a handle on the bubble.
+// * If the user approved, a new profile is created and the token is moved from
+// this profile to the new profile, using DiceSignedInProfileCreator.
+// * At this point, the flow ends in this profile, and continues in the new
+// profile using DiceInterceptedSessionStartupHelper to add the account.
+// * When the account is available on the web in the new profile:
+// - A new browser window is created for the new profile,
+// - The tab is moved to the new profile,
+// - The interception bubble is closed by deleting the handle,
+// - The profile customization bubble is shown.
+class DiceWebSigninInterceptor : public KeyedService,
+ public signin::IdentityManager::Observer {
+ public:
+ enum class SigninInterceptionType {
+ kProfileSwitch,
+ kEnterprise,
+ kMultiUser,
+ kEnterpriseForced,
+ kProfileSwitchForced
+ };
+
+ // Delegate class responsible for showing the various interception UIs.
+ class Delegate {
+ public:
+ // Parameters for interception bubble UIs.
+ struct BubbleParameters {
+ SigninInterceptionType interception_type;
+ AccountInfo intercepted_account;
+ AccountInfo primary_account;
+ SkColor profile_highlight_color;
+ bool show_guest_option;
+ };
+
+ virtual ~Delegate() = default;
+
+ // Shows the signin interception bubble and calls |callback| to indicate
+ // whether the user should continue in a new profile.
+ // The callback is never called if the delegate is deleted before it
+ // completes.
+ // May return a nullptr handle if the bubble cannot be shown.
+ // Warning: the handle closes the bubble when it is destroyed ; it is the
+ // responsibility of the caller to keep the handle alive until the bubble
+ // should be closed.
+ // The callback must not be called synchronously if this function returns a
+ // valid handle (because the caller needs to be able to close the bubble
+ // from the callback).
+ virtual std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle>
+ ShowSigninInterceptionBubble(
+ content::WebContents* web_contents,
+ const BubbleParameters& bubble_parameters,
+ base::OnceCallback<void(SigninInterceptionResult)> callback) = 0;
+
+ // Shows the profile customization bubble.
+ virtual void ShowProfileCustomizationBubble(Browser* browser) = 0;
+ };
+
+ DiceWebSigninInterceptor(Profile* profile,
+ std::unique_ptr<Delegate> delegate);
+ ~DiceWebSigninInterceptor() override;
+
+ DiceWebSigninInterceptor(const DiceWebSigninInterceptor&) = delete;
+ DiceWebSigninInterceptor& operator=(const DiceWebSigninInterceptor&) = delete;
+
+ static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);
+
+ // Called when an account has been added in Chrome from the web (using the
+ // DICE protocol).
+ // |web_contents| is the tab where the signin event happened. It must belong
+ // to the profile associated with this service. It may be nullptr if the tab
+ // was closed.
+ // |is_new_account| is true if the account was not already in Chrome (i.e.
+ // this is not a reauth).
+ // |is_sync_signin| is true if the user is signing in with the intent of
+ // enabling sync for that account.
+ // Virtual for testing.
+ virtual void MaybeInterceptWebSignin(content::WebContents* web_contents,
+ CoreAccountId account_id,
+ bool is_new_account,
+ bool is_sync_signin);
+
+ // Called after the new profile was created during a signin interception.
+ // The token has been moved to the new profile, but the account is not yet in
+ // the cookies.
+ // `intercepted_contents` may be null if the tab was already closed.
+ // The intercepted web contents belong to the source profile (which is not the
+ // profile attached to this service).
+ void CreateBrowserAfterSigninInterception(
+ CoreAccountId account_id,
+ content::WebContents* intercepted_contents,
+ std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle>
+ bubble_handle,
+ bool is_new_profile);
+
+ // Returns the outcome of the interception heuristic.
+ // If the outcome is kInterceptProfileSwitch, the target profile is returned
+ // in |entry|.
+ // In some cases the outcome cannot be fully computed synchronously, when this
+ // happens, the signin interception is highly likely (but not guaranteed).
+ absl::optional<SigninInterceptionHeuristicOutcome> GetHeuristicOutcome(
+ bool is_new_account,
+ bool is_sync_signin,
+ const std::string& email,
+ const ProfileAttributesEntry** entry = nullptr) const;
+
+ // Returns true if the interception is in progress (running the heuristic or
+ // showing on screen).
+ bool is_interception_in_progress() const {
+ return is_interception_in_progress_;
+ }
+
+ void SetAccountLevelSigninRestrictionFetchResultForTesting(
+ absl::optional<std::string> value) {
+ intercepted_account_level_policy_value_fetch_result_for_testing_ =
+ std::move(value);
+ }
+
+ // KeyedService:
+ void Shutdown() override;
+
+ private:
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
+ ShouldShowProfileSwitchBubble);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
+ NoBubbleWithSingleAccount);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
+ ShouldShowEnterpriseBubble);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
+ ShouldShowEnterpriseBubbleWithoutUPA);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest,
+ ShouldShowMultiUserBubble);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest, PersistentHash);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest,
+ ShouldEnforceEnterpriseProfileSeparation);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest,
+ ShouldEnforceEnterpriseProfileSeparationWithoutUPA);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest,
+ ShouldEnforceEnterpriseProfileSeparationReauth);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest,
+ EnforceManagedAccountAsPrimary);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest,
+ ShouldEnforceEnterpriseProfileSeparationReauth);
+ FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorEnterpriseBrowserTest,
+ ForcedEnterpriseInterceptionTestAccountLevelPolicy);
+ FRIEND_TEST_ALL_PREFIXES(
+ DiceWebSigninInterceptorEnterpriseBrowserTest,
+ ForcedEnterpriseInterceptionTestNoForcedInterception);
+
+ // Cancels any current signin interception and resets the interceptor to its
+ // initial state.
+ void Reset();
+
+ // Helper functions to determine which interception UI should be shown.
+ const ProfileAttributesEntry* ShouldShowProfileSwitchBubble(
+ const std::string& intercepted_email,
+ ProfileAttributesStorage* profile_attribute_storage) const;
+ bool ShouldEnforceEnterpriseProfileSeparation(
+ const AccountInfo& intercepted_account_info) const;
+ bool ShouldShowEnterpriseBubble(
+ const AccountInfo& intercepted_account_info) const;
+ bool ShouldShowMultiUserBubble(
+ const AccountInfo& intercepted_account_info) const;
+
+ void OnInterceptionReadyToBeProcessed(const AccountInfo& info);
+
+ // signin::IdentityManager::Observer:
+ void OnExtendedAccountInfoUpdated(const AccountInfo& info) override;
+
+ // Called when the extended account info was not updated after a timeout.
+ void OnExtendedAccountInfoFetchTimeout();
+
+ // Called after the user chose whether a new profile would be created.
+ void OnProfileCreationChoice(const AccountInfo& account_info,
+ SkColor profile_color,
+ SigninInterceptionResult create);
+ // Called after the user chose whether the session should continue in a new
+ // profile.
+ void OnProfileSwitchChoice(const std::string& email,
+ const base::FilePath& profile_path,
+ SigninInterceptionResult switch_profile);
+
+ // Called when the new profile is created or loaded from disk.
+ // `profile_color` is set as theme color for the profile ; it should be
+ // nullopt if the profile is not new (loaded from disk).
+ void OnNewSignedInProfileCreated(absl::optional<SkColor> profile_color,
+ Profile* new_profile);
+
+ // Called after the user choses whether the session should continue in a new
+ // work profile or not. If the user choses not to continue in a work profile,
+ // the account is signed out.
+ void OnEnterpriseProfileCreationResult(const AccountInfo& account_info,
+ SkColor profile_color,
+ SigninInterceptionResult create);
+
+ // Called when the new browser is created after interception. Passed as
+ // callback to `session_startup_helper_`.
+ void OnNewBrowserCreated(bool is_new_profile);
+
+ // Returns a 8-bit hash of the email that can be persisted.
+ static std::string GetPersistentEmailHash(const std::string& email);
+
+ // Should be called when the user declines profile creation, in order to
+ // remember their decision. This information is stored in prefs. Only a hash
+ // of the email is saved, as Chrome does not need to store the actual email,
+ // but only need to compare emails. The hash has low entropy to ensure it
+ // cannot be reversed.
+ void RecordProfileCreationDeclined(const std::string& email);
+
+ // Checks if the user previously declined 2 times creating a new profile for
+ // this account.
+ bool HasUserDeclinedProfileCreation(const std::string& email) const;
+
+ // Fetches the value of the cloud user level value of the
+ // ManagedAccountsSigninRestriction policy for 'account_info' and runs
+ // `callback` with the result. This is a network call that has a 5 seconds
+ // timeout.
+ void FetchAccountLevelSigninRestrictionForInterceptedAccount(
+ const AccountInfo& account_info,
+ base::OnceCallback<void(const std::string&)> callback);
+
+ // Called when the the value of the cloud user level value of the
+ // ManagedAccountsSigninRestriction is received.
+ void OnAccountLevelManagedAccountsSigninRestrictionReceived(
+ bool timed_out,
+ const AccountInfo& account_info,
+ const std::string& signin_restriction);
+
+ const raw_ptr<Profile> profile_;
+ const raw_ptr<signin::IdentityManager> identity_manager_;
+ std::unique_ptr<Delegate> delegate_;
+
+ // Used in the profile that was created after the interception succeeded.
+ std::unique_ptr<DiceInterceptedSessionStartupHelper> session_startup_helper_;
+
+ // Members below are related to the interception in progress.
+ base::WeakPtr<content::WebContents> web_contents_;
+ bool is_interception_in_progress_ = false;
+ CoreAccountId account_id_;
+ bool new_account_interception_ = false;
+ bool intercepted_account_management_accepted_ = false;
+ base::ScopedObservation<signin::IdentityManager,
+ signin::IdentityManager::Observer>
+ account_info_update_observation_{this};
+ // Timeout for the fetch of the extended account info. The signin interception
+ // is cancelled if the account info cannot be fetched quickly.
+ base::CancelableOnceCallback<void()> on_account_info_update_timeout_;
+ std::unique_ptr<DiceSignedInProfileCreator> dice_signed_in_profile_creator_;
+ // Used to retain the interception UI bubble until profile creation completes.
+ std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle>
+ interception_bubble_handle_;
+ // Used for metrics:
+ bool was_interception_ui_displayed_ = false;
+ base::TimeTicks account_info_fetch_start_time_;
+ base::TimeTicks profile_creation_start_time_;
+
+ // Timeout for the fetch of cloud user level policy value of
+ // ManagedAccountsSigninRestriction. The signin interception continue with an
+ // empty value for the policy if we cannot get the value.
+ base::CancelableOnceCallback<void()>
+ on_intercepted_account_level_policy_value_timeout_;
+
+ // Used to fetch the cloud user level policy value of
+ // ManagedAccountsSigninRestriction. This can only fetch one policy value for
+ // one account at the time.
+ std::unique_ptr<policy::UserCloudSigninRestrictionPolicyFetcher>
+ account_level_signin_restriction_policy_fetcher_;
+ // Value of the ManagedAccountsSigninRestriction for the intercepted account.
+ // If no value is set, then we have not yet received the policy value.
+ absl::optional<std::string> intercepted_account_level_policy_value_;
+ absl::optional<std::string>
+ intercepted_account_level_policy_value_fetch_result_for_testing_;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_H_
diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor_browsertest.cc b/chromium/chrome/browser/signin/dice_web_signin_interceptor_browsertest.cc
new file mode 100644
index 00000000000..5a53ea95baa
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor_browsertest.cc
@@ -0,0 +1,1200 @@
+// Copyright 2020 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.
+
+#include "chrome/browser/signin/dice_web_signin_interceptor.h"
+
+#include <map>
+#include <string>
+
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/run_loop.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/test/bind.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "build/build_config.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/enterprise/util/managed_browser_utils.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_init_params.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/profiles/profile_manager_observer.h"
+#include "chrome/browser/profiles/profile_window.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/chrome_signin_client_test_util.h"
+#include "chrome/browser/signin/dice_intercepted_session_startup_helper.h"
+#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "chrome/browser/themes/theme_service.h"
+#include "chrome/browser/themes/theme_service_factory.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/profile_waiter.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/account_id/account_id.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/policy/core/common/features.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/identity_manager/accounts_mutator.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "components/signin/public/identity_manager/primary_account_mutator.h"
+#include "content/public/test/browser_test.h"
+#include "google_apis/gaia/gaia_urls.h"
+#include "services/network/test/test_url_loader_factory.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace {
+
+// Fake response for OAuth multilogin.
+const char kMultiloginSuccessResponse[] =
+ R"()]}'
+ {
+ "status": "OK",
+ "cookies":[
+ {
+ "name":"SID",
+ "value":"SID_value",
+ "domain":".google.fr",
+ "path":"/",
+ "isSecure":true,
+ "isHttpOnly":false,
+ "priority":"HIGH",
+ "maxAge":63070000
+ }
+ ]
+ }
+ )";
+
+class FakeDiceWebSigninInterceptorDelegate;
+
+class FakeBubbleHandle : public ScopedDiceWebSigninInterceptionBubbleHandle,
+ public base::SupportsWeakPtr<FakeBubbleHandle> {
+ public:
+ ~FakeBubbleHandle() override = default;
+};
+
+// Dummy interception delegate that automatically accepts multi user
+// interception.
+class FakeDiceWebSigninInterceptorDelegate
+ : public DiceWebSigninInterceptor::Delegate {
+ public:
+ std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle>
+ ShowSigninInterceptionBubble(
+ content::WebContents* web_contents,
+ const BubbleParameters& bubble_parameters,
+ base::OnceCallback<void(SigninInterceptionResult)> callback) override {
+ EXPECT_EQ(bubble_parameters.interception_type, expected_interception_type_);
+ auto bubble_handle = std::make_unique<FakeBubbleHandle>();
+ weak_bubble_handle_ = bubble_handle->AsWeakPtr();
+ // The callback must not be called synchronously (see the documentation for
+ // ShowSigninInterceptionBubble).
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(std::move(callback), expected_interception_result_));
+ return bubble_handle;
+ }
+
+ void ShowProfileCustomizationBubble(Browser* browser) override {
+ EXPECT_FALSE(customized_browser_)
+ << "Customization must be shown only once.";
+ customized_browser_ = browser;
+ }
+
+ Browser* customized_browser() { return customized_browser_; }
+
+ void set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType type) {
+ expected_interception_type_ = type;
+ }
+
+ void set_expected_interception_result(SigninInterceptionResult result) {
+ expected_interception_result_ = result;
+ }
+
+ bool intercept_bubble_shown() const { return weak_bubble_handle_.get(); }
+
+ bool intercept_bubble_destroyed() const {
+ return weak_bubble_handle_.WasInvalidated();
+ }
+
+ private:
+ raw_ptr<Browser> customized_browser_ = nullptr;
+ DiceWebSigninInterceptor::SigninInterceptionType expected_interception_type_ =
+ DiceWebSigninInterceptor::SigninInterceptionType::kMultiUser;
+ SigninInterceptionResult expected_interception_result_ =
+ SigninInterceptionResult::kAccepted;
+ base::WeakPtr<FakeBubbleHandle> weak_bubble_handle_;
+};
+
+class BrowserCloseObserver : public BrowserListObserver {
+ public:
+ explicit BrowserCloseObserver(Browser* browser) : browser_(browser) {
+ BrowserList::AddObserver(this);
+ }
+
+ BrowserCloseObserver(const BrowserCloseObserver&) = delete;
+ BrowserCloseObserver& operator=(const BrowserCloseObserver&) = delete;
+
+ ~BrowserCloseObserver() override { BrowserList::RemoveObserver(this); }
+
+ void Wait() { run_loop_.Run(); }
+
+ // BrowserListObserver implementation.
+ void OnBrowserRemoved(Browser* browser) override {
+ if (browser == browser_)
+ run_loop_.Quit();
+ }
+
+ private:
+ raw_ptr<Browser> browser_;
+ base::RunLoop run_loop_;
+};
+
+// Runs the interception and returns the new profile that was created.
+Profile* InterceptAndWaitProfileCreation(content::WebContents* contents,
+ const CoreAccountId& account_id) {
+ ProfileWaiter profile_waiter;
+ // Start the interception.
+ DiceWebSigninInterceptor* interceptor =
+ DiceWebSigninInterceptorFactory::GetForProfile(
+ Profile::FromBrowserContext(contents->GetBrowserContext()));
+ interceptor->MaybeInterceptWebSignin(contents, account_id,
+ /*is_new_account=*/true,
+ /*is_sync_signin=*/false);
+ // Wait for the interception to be complete.
+ return profile_waiter.WaitForProfileAdded();
+}
+
+// Checks that the interception histograms were correctly recorded.
+void CheckHistograms(const base::HistogramTester& histogram_tester,
+ SigninInterceptionHeuristicOutcome outcome,
+ bool reauth = false) {
+ int profile_switch_count =
+ outcome == SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch ||
+ outcome == SigninInterceptionHeuristicOutcome::
+ kInterceptEnterpriseForcedProfileSwitch
+ ? 1
+ : 0;
+ int profile_creation_count = reauth ? 0 : 1 - profile_switch_count;
+ int fetched_account_count =
+ reauth || outcome == SigninInterceptionHeuristicOutcome::
+ kInterceptEnterpriseForcedProfileSwitch
+ ? 1
+ : profile_creation_count;
+
+ histogram_tester.ExpectUniqueSample("Signin.Intercept.HeuristicOutcome",
+ outcome, 1);
+ histogram_tester.ExpectTotalCount(
+ "Signin.Intercept.AccountInfoFetchDuration",
+ base::FeatureList::IsEnabled(
+ policy::features::kEnableUserCloudSigninRestrictionPolicyFetcher)
+ ? 1
+ : fetched_account_count);
+ histogram_tester.ExpectTotalCount("Signin.Intercept.ProfileCreationDuration",
+ profile_creation_count);
+ histogram_tester.ExpectTotalCount("Signin.Intercept.ProfileSwitchDuration",
+ profile_switch_count);
+}
+
+} // namespace
+
+class DiceWebSigninInterceptorBrowserTest : public InProcessBrowserTest {
+ public:
+ DiceWebSigninInterceptorBrowserTest() = default;
+
+ Profile* profile() { return browser()->profile(); }
+
+ signin::IdentityTestEnvironment* identity_test_env() {
+ return identity_test_env_profile_adaptor_->identity_test_env();
+ }
+
+ network::TestURLLoaderFactory* test_url_loader_factory() {
+ return &test_url_loader_factory_;
+ }
+
+ content::WebContents* AddTab(const GURL& url) {
+ ui_test_utils::NavigateToURLWithDisposition(
+ browser(), url, WindowOpenDisposition::NEW_FOREGROUND_TAB,
+ ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
+ return browser()->tab_strip_model()->GetActiveWebContents();
+ }
+
+ FakeDiceWebSigninInterceptorDelegate* GetInterceptorDelegate(
+ Profile* profile) {
+ // Make sure the interceptor has been created.
+ DiceWebSigninInterceptorFactory::GetForProfile(profile);
+ FakeDiceWebSigninInterceptorDelegate* interceptor_delegate =
+ interceptor_delegates_[profile];
+ return interceptor_delegate;
+ }
+
+ void SetupGaiaResponses() {
+ // Instantly return from Gaia calls, to avoid timing out when injecting the
+ // account in the new profile.
+ network::TestURLLoaderFactory* loader_factory = test_url_loader_factory();
+ loader_factory->SetInterceptor(base::BindLambdaForTesting(
+ [loader_factory](const network::ResourceRequest& request) {
+ std::string path = request.url.path();
+ if (path == "/ListAccounts" || path == "/GetCheckConnectionInfo") {
+ loader_factory->AddResponse(request.url.spec(), std::string());
+ return;
+ }
+ if (path == "/oauth/multilogin") {
+ loader_factory->AddResponse(request.url.spec(),
+ kMultiloginSuccessResponse);
+ return;
+ }
+ }));
+ }
+
+ private:
+ // InProcessBrowserTest:
+ void SetUpOnMainThread() override {
+ ASSERT_TRUE(embedded_test_server()->Start());
+ identity_test_env_profile_adaptor_ =
+ std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile());
+ DiceWebSigninInterceptorFactory::GetForProfile(profile())
+ ->SetAccountLevelSigninRestrictionFetchResultForTesting("");
+ }
+
+ void TearDownOnMainThread() override {
+ // Must be destroyed before the Profile.
+ identity_test_env_profile_adaptor_.reset();
+ }
+
+ void SetUpInProcessBrowserTestFixture() override {
+ InProcessBrowserTest::SetUpInProcessBrowserTestFixture();
+ create_services_subscription_ =
+ BrowserContextDependencyManager::GetInstance()
+ ->RegisterCreateServicesCallbackForTesting(
+ base::BindRepeating(&DiceWebSigninInterceptorBrowserTest::
+ OnWillCreateBrowserContextServices,
+ base::Unretained(this)));
+ }
+
+ void OnWillCreateBrowserContextServices(content::BrowserContext* context) {
+ IdentityTestEnvironmentProfileAdaptor::
+ SetIdentityTestEnvironmentFactoriesOnBrowserContext(context);
+ ChromeSigninClientFactory::GetInstance()->SetTestingFactory(
+ context, base::BindRepeating(&BuildChromeSigninClientWithURLLoader,
+ &test_url_loader_factory_));
+ DiceWebSigninInterceptorFactory::GetInstance()->SetTestingFactory(
+ context,
+ base::BindRepeating(&DiceWebSigninInterceptorBrowserTest::
+ BuildDiceWebSigninInterceptorWithFakeDelegate,
+ base::Unretained(this)));
+ }
+
+ // Builds a DiceWebSigninInterceptor with a fake delegate. To be used as a
+ // testing factory.
+ std::unique_ptr<KeyedService> BuildDiceWebSigninInterceptorWithFakeDelegate(
+ content::BrowserContext* context) {
+ std::unique_ptr<FakeDiceWebSigninInterceptorDelegate> fake_delegate =
+ std::make_unique<FakeDiceWebSigninInterceptorDelegate>();
+ interceptor_delegates_[context] = fake_delegate.get();
+ return std::make_unique<DiceWebSigninInterceptor>(
+ Profile::FromBrowserContext(context), std::move(fake_delegate));
+ }
+
+ network::TestURLLoaderFactory test_url_loader_factory_;
+ std::unique_ptr<IdentityTestEnvironmentProfileAdaptor>
+ identity_test_env_profile_adaptor_;
+ base::CallbackListSubscription create_services_subscription_;
+ std::map<content::BrowserContext*, FakeDiceWebSigninInterceptorDelegate*>
+ interceptor_delegates_;
+};
+
+// Tests the complete interception flow including profile and browser creation.
+IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorBrowserTest, InterceptionTest) {
+ base::HistogramTester histogram_tester;
+ // Setup profile for interception.
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = kNoHostedDomainFound;
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ SetupGaiaResponses();
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+
+ // Do the signin interception.
+ EXPECT_EQ(BrowserList::GetInstance()->size(), 1u);
+ Profile* new_profile =
+ InterceptAndWaitProfileCreation(web_contents, account_info.account_id);
+ ASSERT_TRUE(new_profile);
+ FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate =
+ GetInterceptorDelegate(profile());
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown());
+ signin::IdentityManager* new_identity_manager =
+ IdentityManagerFactory::GetForProfile(new_profile);
+ EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken(
+ account_info.account_id));
+
+ IdentityTestEnvironmentProfileAdaptor adaptor(new_profile);
+ adaptor.identity_test_env()->SetAutomaticIssueOfAccessTokens(true);
+
+ // Check the profile name.
+ ProfileAttributesStorage& storage =
+ g_browser_process->profile_manager()->GetProfileAttributesStorage();
+ ProfileAttributesEntry* entry =
+ storage.GetProfileAttributesWithPath(new_profile->GetPath());
+ ASSERT_TRUE(entry);
+ EXPECT_EQ("givenname", base::UTF16ToUTF8(entry->GetLocalProfileName()));
+ // Check the profile color.
+ EXPECT_TRUE(ThemeServiceFactory::GetForProfile(new_profile)
+ ->UsingAutogeneratedTheme());
+
+ // A browser has been created for the new profile and the tab was moved there.
+ Browser* added_browser = ui_test_utils::WaitForBrowserToOpen();
+ ASSERT_TRUE(added_browser);
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ EXPECT_EQ(added_browser->profile(), new_profile);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1);
+ EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(histogram_tester,
+ SigninInterceptionHeuristicOutcome::kInterceptMultiUser);
+ // Interception bubble is destroyed in the source profile, and was not shown
+ // in the new profile.
+ FakeDiceWebSigninInterceptorDelegate* new_interceptor_delegate =
+ GetInterceptorDelegate(new_profile);
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_destroyed());
+ EXPECT_FALSE(new_interceptor_delegate->intercept_bubble_shown());
+ EXPECT_FALSE(new_interceptor_delegate->intercept_bubble_destroyed());
+ // Profile customization UI was shown exactly once in the new profile.
+ EXPECT_EQ(new_interceptor_delegate->customized_browser(), added_browser);
+ EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr);
+}
+
+// Tests the complete profile switch flow when the profile is not loaded.
+IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorBrowserTest, SwitchAndLoad) {
+ base::HistogramTester histogram_tester;
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = "example.com";
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ // Add a profile in the cache (simulate the profile on disk).
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ ProfileAttributesStorage* profile_storage =
+ &profile_manager->GetProfileAttributesStorage();
+ const base::FilePath profile_path =
+ profile_manager->GenerateNextProfileDirectoryPath();
+ ProfileAttributesInitParams params;
+ params.profile_path = profile_path;
+ params.profile_name = u"TestProfileName";
+ params.gaia_id = account_info.gaia;
+ params.user_name = base::UTF8ToUTF16(account_info.email);
+ profile_storage->AddProfile(std::move(params));
+ ProfileAttributesEntry* entry =
+ profile_storage->GetProfileAttributesWithPath(profile_path);
+ ASSERT_TRUE(entry);
+ ASSERT_EQ(entry->GetGAIAId(), account_info.gaia);
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+
+ // Do the signin interception.
+ FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate =
+ GetInterceptorDelegate(profile());
+ source_interceptor_delegate->set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch);
+ Profile* new_profile =
+ InterceptAndWaitProfileCreation(web_contents, account_info.account_id);
+ ASSERT_TRUE(new_profile);
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown());
+ signin::IdentityManager* new_identity_manager =
+ IdentityManagerFactory::GetForProfile(new_profile);
+ EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken(
+ account_info.account_id));
+
+ // Check that the right profile was opened.
+ EXPECT_EQ(new_profile->GetPath(), profile_path);
+
+ // Add the account to the cookies (simulates the account reconcilor).
+ EXPECT_EQ(BrowserList::GetInstance()->size(), 1u);
+ signin::SetCookieAccounts(new_identity_manager, test_url_loader_factory(),
+ {{account_info.email, account_info.gaia}});
+
+ // A browser has been created for the new profile and the tab was moved there.
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ Browser* added_browser = BrowserList::GetInstance()->get(1);
+ ASSERT_TRUE(added_browser);
+ EXPECT_EQ(added_browser->profile(), new_profile);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1);
+ EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(histogram_tester,
+ SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch);
+ // Interception bubble was closed.
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_destroyed());
+ // Profile customization was not shown.
+ EXPECT_EQ(GetInterceptorDelegate(new_profile)->customized_browser(), nullptr);
+ EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr);
+}
+
+// Tests the complete profile switch flow when the profile is already loaded.
+IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorBrowserTest, SwitchAlreadyOpen) {
+ base::HistogramTester histogram_tester;
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = "example.com";
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ // Create another profile with a browser window.
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ const base::FilePath profile_path =
+ profile_manager->GenerateNextProfileDirectoryPath();
+ base::RunLoop loop;
+ Profile* other_profile = nullptr;
+ ProfileManager::CreateCallback callback = base::BindLambdaForTesting(
+ [&other_profile, &loop](Profile* profile, Profile::CreateStatus status) {
+ DCHECK_EQ(status, Profile::CREATE_STATUS_INITIALIZED);
+ other_profile = profile;
+ loop.Quit();
+ });
+ profiles::SwitchToProfile(profile_path, /*always_create=*/true,
+ std::move(callback));
+ loop.Run();
+ ASSERT_TRUE(other_profile);
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ Browser* other_browser = BrowserList::GetInstance()->get(1);
+ ASSERT_TRUE(other_browser);
+ ASSERT_EQ(other_browser->profile(), other_profile);
+ // Add the account to the other profile.
+ signin::IdentityManager* other_identity_manager =
+ IdentityManagerFactory::GetForProfile(other_profile);
+ other_identity_manager->GetAccountsMutator()->AddOrUpdateAccount(
+ account_info.gaia, account_info.email, "dummy_refresh_token",
+ /*is_under_advanced_protection=*/false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+ other_identity_manager->GetPrimaryAccountMutator()->SetPrimaryAccount(
+ account_info.account_id, signin::ConsentLevel::kSync);
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+ int other_original_tab_count = other_browser->tab_strip_model()->count();
+
+ // Start the interception.
+ GetInterceptorDelegate(profile())->set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch);
+ DiceWebSigninInterceptor* interceptor =
+ DiceWebSigninInterceptorFactory::GetForProfile(profile());
+ interceptor->MaybeInterceptWebSignin(web_contents, account_info.account_id,
+ /*is_new_account=*/true,
+ /*is_sync_signin=*/false);
+
+ // Add the account to the cookies (simulates the account reconcilor).
+ signin::SetCookieAccounts(other_identity_manager, test_url_loader_factory(),
+ {{account_info.email, account_info.gaia}});
+
+ // The tab was moved to the new browser.
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1);
+ EXPECT_EQ(other_browser->tab_strip_model()->count(),
+ other_original_tab_count + 1);
+ EXPECT_EQ(other_browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(histogram_tester,
+ SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch);
+ // Profile customization was not shown.
+ EXPECT_EQ(GetInterceptorDelegate(other_profile)->customized_browser(),
+ nullptr);
+ EXPECT_EQ(GetInterceptorDelegate(profile())->customized_browser(), nullptr);
+}
+
+// Close the source tab during the interception and check that the NTP is opened
+// in the new profile (regression test for https://crbug.com/1153321).
+IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorBrowserTest, CloseSourceTab) {
+ // Setup profile for interception.
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = kNoHostedDomainFound;
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+
+ // Do the signin interception.
+ ProfileWaiter profile_waiter;
+ DiceWebSigninInterceptor* interceptor =
+ DiceWebSigninInterceptorFactory::GetForProfile(
+ Profile::FromBrowserContext(contents->GetBrowserContext()));
+ interceptor->MaybeInterceptWebSignin(contents, account_info.account_id,
+ /*is_new_account=*/true,
+ /*is_sync_signin=*/false);
+ // Close the source tab during the profile creation.
+ contents->Close();
+ // Wait for the interception to be complete.
+ Profile* new_profile = profile_waiter.WaitForProfileAdded();
+ ASSERT_TRUE(new_profile);
+ signin::IdentityManager* new_identity_manager =
+ IdentityManagerFactory::GetForProfile(new_profile);
+ EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken(
+ account_info.account_id));
+
+ // Add the account to the cookies (simulates the account reconcilor).
+ EXPECT_EQ(BrowserList::GetInstance()->size(), 1u);
+ signin::SetCookieAccounts(new_identity_manager, test_url_loader_factory(),
+ {{account_info.email, account_info.gaia}});
+
+ // A browser has been created for the new profile on the new tab page.
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ Browser* added_browser = BrowserList::GetInstance()->get(1);
+ ASSERT_TRUE(added_browser);
+ EXPECT_EQ(added_browser->profile(), new_profile);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1);
+ EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ GURL("chrome://newtab/"));
+}
+
+class DiceWebSigninInterceptorEnterpriseBrowserTest
+ : public DiceWebSigninInterceptorBrowserTest {
+ public:
+ DiceWebSigninInterceptorEnterpriseBrowserTest() {
+ enterprise_feature_list_.InitWithFeatures(
+ {kAccountPoliciesLoadedWithoutSync,
+ policy::features::kEnableUserCloudSigninRestrictionPolicyFetcher},
+ {});
+ }
+
+ private:
+ base::test::ScopedFeatureList enterprise_feature_list_;
+};
+
+// Tests the complete interception flow including profile and browser creation.
+IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest,
+ ForcedEnterpriseInterceptionTestNoForcedInterception) {
+ base::HistogramTester histogram_tester;
+
+ AccountInfo primary_account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ primary_account_info.full_name = "fullname";
+ primary_account_info.given_name = "givenname";
+ primary_account_info.hosted_domain = "example.com";
+ primary_account_info.locale = "en";
+ primary_account_info.picture_url = "https://example.com";
+ DCHECK(primary_account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(primary_account_info);
+ IdentityManagerFactory::GetForProfile(profile())
+ ->GetPrimaryAccountMutator()
+ ->SetPrimaryAccount(primary_account_info.account_id,
+ signin::ConsentLevel::kSync);
+
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = "example.com";
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ // Enforce enterprise profile sepatation.
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "none");
+ DiceWebSigninInterceptorFactory::GetForProfile(profile())
+ ->SetAccountLevelSigninRestrictionFetchResultForTesting("");
+
+ // Instantly return from Gaia calls, to avoid timing out when injecting the
+ // account in the new profile.
+ network::TestURLLoaderFactory* loader_factory = test_url_loader_factory();
+ loader_factory->SetInterceptor(base::BindLambdaForTesting(
+ [loader_factory](const network::ResourceRequest& request) {
+ std::string path = request.url.path();
+ if (path == "/ListAccounts" || path == "/GetCheckConnectionInfo") {
+ loader_factory->AddResponse(request.url.spec(), std::string());
+ return;
+ }
+ if (path == "/oauth/multilogin") {
+ loader_factory->AddResponse(request.url.spec(),
+ kMultiloginSuccessResponse);
+ return;
+ }
+ }));
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+
+ // Do the signin interception.
+ EXPECT_EQ(BrowserList::GetInstance()->size(), 1u);
+ FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate =
+ GetInterceptorDelegate(profile());
+ source_interceptor_delegate->set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise);
+ Profile* new_profile =
+ InterceptAndWaitProfileCreation(web_contents, account_info.account_id);
+ EXPECT_FALSE(
+ chrome::enterprise_util::UserAcceptedAccountManagement(new_profile));
+ ASSERT_TRUE(new_profile);
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown());
+ signin::IdentityManager* new_identity_manager =
+ IdentityManagerFactory::GetForProfile(new_profile);
+ EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken(
+ account_info.account_id));
+
+ IdentityTestEnvironmentProfileAdaptor adaptor(new_profile);
+ adaptor.identity_test_env()->SetAutomaticIssueOfAccessTokens(true);
+
+ // Check the profile name.
+ ProfileAttributesStorage& storage =
+ g_browser_process->profile_manager()->GetProfileAttributesStorage();
+ ProfileAttributesEntry* entry =
+ storage.GetProfileAttributesWithPath(new_profile->GetPath());
+ ASSERT_TRUE(entry);
+ EXPECT_EQ("example.com", base::UTF16ToUTF8(entry->GetLocalProfileName()));
+ // Check the profile color.
+ EXPECT_TRUE(ThemeServiceFactory::GetForProfile(new_profile)
+ ->UsingAutogeneratedTheme());
+
+ // A browser has been created for the new profile and the tab was moved there.
+ Browser* added_browser = ui_test_utils::WaitForBrowserToOpen();
+ ASSERT_TRUE(added_browser);
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ EXPECT_EQ(added_browser->profile(), new_profile);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1);
+ EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(histogram_tester,
+ SigninInterceptionHeuristicOutcome::kInterceptEnterprise);
+ // Interception bubble is destroyed in the source profile, and was not shown
+ // in the new profile.
+ FakeDiceWebSigninInterceptorDelegate* new_interceptor_delegate =
+ GetInterceptorDelegate(new_profile);
+
+ // Profile customization UI was shown exactly once in the new profile.
+ EXPECT_EQ(new_interceptor_delegate->customized_browser(), added_browser);
+ EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr);
+}
+
+// Tests the complete interception flow including profile and browser creation.
+IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest,
+ ForcedEnterpriseInterceptionTestAccountLevelPolicy) {
+ base::HistogramTester histogram_tester;
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = "example.com";
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ // Enforce enterprise profile sepatation.
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "none");
+ DiceWebSigninInterceptorFactory::GetForProfile(profile())
+ ->SetAccountLevelSigninRestrictionFetchResultForTesting(
+ "primary_account");
+
+ // Instantly return from Gaia calls, to avoid timing out when injecting the
+ // account in the new profile.
+ network::TestURLLoaderFactory* loader_factory = test_url_loader_factory();
+ loader_factory->SetInterceptor(base::BindLambdaForTesting(
+ [loader_factory](const network::ResourceRequest& request) {
+ std::string path = request.url.path();
+ if (path == "/ListAccounts" || path == "/GetCheckConnectionInfo") {
+ loader_factory->AddResponse(request.url.spec(), std::string());
+ return;
+ }
+ if (path == "/oauth/multilogin") {
+ loader_factory->AddResponse(request.url.spec(),
+ kMultiloginSuccessResponse);
+ return;
+ }
+ }));
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+
+ // Do the signin interception.
+ EXPECT_EQ(BrowserList::GetInstance()->size(), 1u);
+ FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate =
+ GetInterceptorDelegate(profile());
+ source_interceptor_delegate->set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced);
+ Profile* new_profile =
+ InterceptAndWaitProfileCreation(web_contents, account_info.account_id);
+ EXPECT_TRUE(
+ chrome::enterprise_util::UserAcceptedAccountManagement(new_profile));
+ ASSERT_TRUE(new_profile);
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown());
+ signin::IdentityManager* new_identity_manager =
+ IdentityManagerFactory::GetForProfile(new_profile);
+ EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken(
+ account_info.account_id));
+
+ IdentityTestEnvironmentProfileAdaptor adaptor(new_profile);
+ adaptor.identity_test_env()->SetAutomaticIssueOfAccessTokens(true);
+
+ // Check the profile name.
+ ProfileAttributesStorage& storage =
+ g_browser_process->profile_manager()->GetProfileAttributesStorage();
+ ProfileAttributesEntry* entry =
+ storage.GetProfileAttributesWithPath(new_profile->GetPath());
+ ASSERT_TRUE(entry);
+ EXPECT_EQ("example.com", base::UTF16ToUTF8(entry->GetLocalProfileName()));
+ // Check the profile color.
+ EXPECT_TRUE(ThemeServiceFactory::GetForProfile(new_profile)
+ ->UsingAutogeneratedTheme());
+
+ // A browser has been created for the new profile and the tab was moved there.
+ Browser* added_browser = ui_test_utils::WaitForBrowserToOpen();
+ ASSERT_TRUE(added_browser);
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ EXPECT_EQ(added_browser->profile(), new_profile);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1);
+ EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(
+ histogram_tester,
+ SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced);
+ // Interception bubble is destroyed in the source profile, and was not shown
+ // in the new profile.
+ FakeDiceWebSigninInterceptorDelegate* new_interceptor_delegate =
+ GetInterceptorDelegate(new_profile);
+
+ // Profile customization UI was shown exactly once in the new profile.
+ EXPECT_EQ(new_interceptor_delegate->customized_browser(), added_browser);
+ EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr);
+}
+
+// Tests the complete interception flow including profile and browser creation.
+IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest,
+ ForcedEnterpriseInterceptionTest) {
+ base::HistogramTester histogram_tester;
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = "example.com";
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ // Enforce enterprise profile separation.
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+
+ SetupGaiaResponses();
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+
+ // Do the signin interception.
+ EXPECT_EQ(BrowserList::GetInstance()->size(), 1u);
+ FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate =
+ GetInterceptorDelegate(profile());
+ source_interceptor_delegate->set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced);
+ Profile* new_profile =
+ InterceptAndWaitProfileCreation(web_contents, account_info.account_id);
+ EXPECT_TRUE(
+ chrome::enterprise_util::UserAcceptedAccountManagement(new_profile));
+ ASSERT_TRUE(new_profile);
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown());
+ signin::IdentityManager* new_identity_manager =
+ IdentityManagerFactory::GetForProfile(new_profile);
+ EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken(
+ account_info.account_id));
+
+ IdentityTestEnvironmentProfileAdaptor adaptor(new_profile);
+ adaptor.identity_test_env()->SetAutomaticIssueOfAccessTokens(true);
+
+ // Check the profile name.
+ ProfileAttributesStorage& storage =
+ g_browser_process->profile_manager()->GetProfileAttributesStorage();
+ ProfileAttributesEntry* entry =
+ storage.GetProfileAttributesWithPath(new_profile->GetPath());
+ ASSERT_TRUE(entry);
+ EXPECT_EQ("example.com", base::UTF16ToUTF8(entry->GetLocalProfileName()));
+ // Check the profile color.
+ EXPECT_TRUE(ThemeServiceFactory::GetForProfile(new_profile)
+ ->UsingAutogeneratedTheme());
+
+ // A browser has been created for the new profile and the tab was moved there.
+ Browser* added_browser = ui_test_utils::WaitForBrowserToOpen();
+ ASSERT_TRUE(added_browser);
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ EXPECT_EQ(added_browser->profile(), new_profile);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1);
+ EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(
+ histogram_tester,
+ SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced);
+ // Interception bubble is destroyed in the source profile, and was not shown
+ // in the new profile.
+ FakeDiceWebSigninInterceptorDelegate* new_interceptor_delegate =
+ GetInterceptorDelegate(new_profile);
+
+ // Profile customization UI was shown exactly once in the new profile.
+ EXPECT_EQ(new_interceptor_delegate->customized_browser(), added_browser);
+ EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr);
+}
+
+// Tests the complete interception flow for a reauth of the primary account of a
+// non-syncing profile.
+IN_PROC_BROWSER_TEST_F(
+ DiceWebSigninInterceptorEnterpriseBrowserTest,
+ ForcedEnterpriseInterceptionPrimaryACcountReauthSyncDisabledTest) {
+ base::HistogramTester histogram_tester;
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = "example.com";
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ IdentityManagerFactory::GetForProfile(profile())
+ ->GetPrimaryAccountMutator()
+ ->SetPrimaryAccount(account_info.account_id,
+ signin::ConsentLevel::kSignin);
+
+ // Enforce enterprise profile separation.
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+
+ SetupGaiaResponses();
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+
+ // Do the signin interception.
+ EXPECT_EQ(BrowserList::GetInstance()->size(), 1u);
+ FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate =
+ GetInterceptorDelegate(profile());
+ source_interceptor_delegate->set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced);
+
+ EXPECT_FALSE(
+ chrome::enterprise_util::UserAcceptedAccountManagement(profile()));
+ // Start the interception.
+ DiceWebSigninInterceptor* interceptor =
+ DiceWebSigninInterceptorFactory::GetForProfile(profile());
+ interceptor->MaybeInterceptWebSignin(web_contents, account_info.account_id,
+ /*is_new_account=*/false,
+ /*is_sync_signin=*/false);
+ base::RunLoop().RunUntilIdle();
+ EXPECT_TRUE(
+ chrome::enterprise_util::UserAcceptedAccountManagement(profile()));
+ // Interception bubble was closed.
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_destroyed());
+ EXPECT_TRUE(IdentityManagerFactory::GetForProfile(profile())
+ ->HasAccountWithRefreshToken(account_info.account_id));
+
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 1u);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count);
+ EXPECT_EQ(browser()->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(
+ histogram_tester,
+ SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced,
+ /*reauth=*/true);
+}
+
+// Tests the complete interception flow for a reauth of the primary account of a
+// syncing profile.
+IN_PROC_BROWSER_TEST_F(
+ DiceWebSigninInterceptorEnterpriseBrowserTest,
+ ForcedEnterpriseInterceptionPrimaryACcountReauthSyncEnabledTest) {
+ base::HistogramTester histogram_tester;
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = "example.com";
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ IdentityManagerFactory::GetForProfile(profile())
+ ->GetPrimaryAccountMutator()
+ ->SetPrimaryAccount(account_info.account_id, signin::ConsentLevel::kSync);
+
+ // Enforce enterprise profile separation.
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+
+ SetupGaiaResponses();
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+
+ // Do the signin interception.
+ EXPECT_EQ(BrowserList::GetInstance()->size(), 1u);
+ FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate =
+ GetInterceptorDelegate(profile());
+ source_interceptor_delegate->set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced);
+
+ EXPECT_FALSE(
+ chrome::enterprise_util::UserAcceptedAccountManagement(profile()));
+ // Start the interception.
+ DiceWebSigninInterceptor* interceptor =
+ DiceWebSigninInterceptorFactory::GetForProfile(profile());
+ interceptor->MaybeInterceptWebSignin(web_contents, account_info.account_id,
+ /*is_new_account=*/false,
+ /*is_sync_signin=*/false);
+ base::RunLoop().RunUntilIdle();
+ EXPECT_TRUE(
+ chrome::enterprise_util::UserAcceptedAccountManagement(profile()));
+ // Interception bubble was closed.
+ EXPECT_FALSE(source_interceptor_delegate->intercept_bubble_shown());
+ EXPECT_FALSE(source_interceptor_delegate->intercept_bubble_destroyed());
+ EXPECT_TRUE(IdentityManagerFactory::GetForProfile(profile())
+ ->HasAccountWithRefreshToken(account_info.account_id));
+
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 1u);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count);
+ EXPECT_EQ(browser()->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(histogram_tester,
+ SigninInterceptionHeuristicOutcome::kAbortAccountNotNew,
+ /*reauth=*/true);
+}
+
+// Tests the complete profile switch flow when the profile is not loaded.
+IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest,
+ EnterpriseSwitchAndLoad) {
+ base::HistogramTester histogram_tester;
+ // Enforce enterprise profile separation.
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = "example.com";
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ // Add a profile in the cache (simulate the profile on disk).
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ ProfileAttributesStorage* profile_storage =
+ &profile_manager->GetProfileAttributesStorage();
+ const base::FilePath profile_path =
+ profile_manager->GenerateNextProfileDirectoryPath();
+ ProfileAttributesInitParams params;
+ params.profile_path = profile_path;
+ params.profile_name = u"TestProfileName";
+ params.gaia_id = account_info.gaia;
+ params.user_name = base::UTF8ToUTF16(account_info.email);
+ profile_storage->AddProfile(std::move(params));
+ ProfileAttributesEntry* entry =
+ profile_storage->GetProfileAttributesWithPath(profile_path);
+ ASSERT_TRUE(entry);
+ ASSERT_EQ(entry->GetGAIAId(), account_info.gaia);
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+
+ // Do the signin interception.
+ FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate =
+ GetInterceptorDelegate(profile());
+ source_interceptor_delegate->set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitchForced);
+ Profile* new_profile =
+ InterceptAndWaitProfileCreation(web_contents, account_info.account_id);
+ ASSERT_TRUE(new_profile);
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown());
+ signin::IdentityManager* new_identity_manager =
+ IdentityManagerFactory::GetForProfile(new_profile);
+ EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken(
+ account_info.account_id));
+
+ // Check that the right profile was opened.
+ EXPECT_EQ(new_profile->GetPath(), profile_path);
+
+ // Add the account to the cookies (simulates the account reconcilor).
+ EXPECT_EQ(BrowserList::GetInstance()->size(), 1u);
+ signin::SetCookieAccounts(new_identity_manager, test_url_loader_factory(),
+ {{account_info.email, account_info.gaia}});
+
+ // A browser has been created for the new profile and the tab was moved there.
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ Browser* added_browser = BrowserList::GetInstance()->get(1);
+ ASSERT_TRUE(added_browser);
+ EXPECT_EQ(added_browser->profile(), new_profile);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1);
+ EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(histogram_tester,
+ SigninInterceptionHeuristicOutcome::
+ kInterceptEnterpriseForcedProfileSwitch);
+
+ // Interception bubble was closed.
+ EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_destroyed());
+
+ // Profile customization was not shown.
+ EXPECT_EQ(GetInterceptorDelegate(new_profile)->customized_browser(), nullptr);
+ EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr);
+}
+
+// Failed run on MAC CI builder. https://crbug.com/1245200
+#if defined(OS_MAC)
+#define MAYBE_EnterpriseSwitchAlreadyOpen DISABLED_EnterpriseSwitchAlreadyOpen
+#else
+#define MAYBE_EnterpriseSwitchAlreadyOpen EnterpriseSwitchAlreadyOpen
+#endif
+// Tests the complete profile switch flow when the profile is already loaded.
+IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest,
+ MAYBE_EnterpriseSwitchAlreadyOpen) {
+ base::HistogramTester histogram_tester;
+ // Enforce enterprise profile separation.
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ // Fill the account info, in particular for the hosted_domain field.
+ account_info.full_name = "fullname";
+ account_info.given_name = "givenname";
+ account_info.hosted_domain = "example.com";
+ account_info.locale = "en";
+ account_info.picture_url = "https://example.com";
+ DCHECK(account_info.IsValid());
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ // Create another profile with a browser window.
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ const base::FilePath profile_path =
+ profile_manager->GenerateNextProfileDirectoryPath();
+ base::RunLoop loop;
+ Profile* other_profile = nullptr;
+ ProfileManager::CreateCallback callback = base::BindLambdaForTesting(
+ [&other_profile, &loop](Profile* profile, Profile::CreateStatus status) {
+ DCHECK_EQ(status, Profile::CREATE_STATUS_INITIALIZED);
+ other_profile = profile;
+ loop.Quit();
+ });
+ profiles::SwitchToProfile(profile_path, /*always_create=*/true,
+ std::move(callback));
+ loop.Run();
+ ASSERT_TRUE(other_profile);
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ Browser* other_browser = BrowserList::GetInstance()->get(1);
+ ASSERT_TRUE(other_browser);
+ ASSERT_EQ(other_browser->profile(), other_profile);
+ // Add the account to the other profile.
+ signin::IdentityManager* other_identity_manager =
+ IdentityManagerFactory::GetForProfile(other_profile);
+ other_identity_manager->GetAccountsMutator()->AddOrUpdateAccount(
+ account_info.gaia, account_info.email, "dummy_refresh_token",
+ /*is_under_advanced_protection=*/false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+ other_identity_manager->GetPrimaryAccountMutator()->SetPrimaryAccount(
+ account_info.account_id, signin::ConsentLevel::kSync);
+
+ // Add a tab.
+ GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse");
+ content::WebContents* web_contents = AddTab(intercepted_url);
+ int original_tab_count = browser()->tab_strip_model()->count();
+ int other_original_tab_count = other_browser->tab_strip_model()->count();
+
+ // Start the interception.
+ GetInterceptorDelegate(profile())->set_expected_interception_type(
+ DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitchForced);
+ DiceWebSigninInterceptor* interceptor =
+ DiceWebSigninInterceptorFactory::GetForProfile(profile());
+ interceptor->MaybeInterceptWebSignin(web_contents, account_info.account_id,
+ /*is_new_account=*/true,
+ /*is_sync_signin=*/false);
+
+ // Add the account to the cookies (simulates the account reconcilor).
+ signin::SetCookieAccounts(other_identity_manager, test_url_loader_factory(),
+ {{account_info.email, account_info.gaia}});
+
+ // The tab was moved to the new browser.
+ ASSERT_EQ(BrowserList::GetInstance()->size(), 2u);
+ EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1);
+ EXPECT_EQ(other_browser->tab_strip_model()->count(),
+ other_original_tab_count + 1);
+ EXPECT_EQ(other_browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
+ intercepted_url);
+
+ CheckHistograms(histogram_tester,
+ SigninInterceptionHeuristicOutcome::
+ kInterceptEnterpriseForcedProfileSwitch);
+ // Profile customization was not shown.
+ EXPECT_EQ(GetInterceptorDelegate(other_profile)->customized_browser(),
+ nullptr);
+ EXPECT_EQ(GetInterceptorDelegate(profile())->customized_browser(), nullptr);
+}
diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.cc b/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.cc
new file mode 100644
index 00000000000..9377d94d2a8
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.cc
@@ -0,0 +1,45 @@
+// Copyright 2020 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.
+
+#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h"
+
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/dice_web_signin_interceptor.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/ui/signin/dice_web_signin_interceptor_delegate.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+
+// static
+DiceWebSigninInterceptor* DiceWebSigninInterceptorFactory::GetForProfile(
+ Profile* profile) {
+ return static_cast<DiceWebSigninInterceptor*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+DiceWebSigninInterceptorFactory*
+DiceWebSigninInterceptorFactory::GetInstance() {
+ return base::Singleton<DiceWebSigninInterceptorFactory>::get();
+}
+
+DiceWebSigninInterceptorFactory::DiceWebSigninInterceptorFactory()
+ : BrowserContextKeyedServiceFactory(
+ "DiceWebSigninInterceptor",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(IdentityManagerFactory::GetInstance());
+}
+
+DiceWebSigninInterceptorFactory::~DiceWebSigninInterceptorFactory() = default;
+
+void DiceWebSigninInterceptorFactory::RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) {
+ DiceWebSigninInterceptor::RegisterProfilePrefs(registry);
+}
+
+KeyedService* DiceWebSigninInterceptorFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ return new DiceWebSigninInterceptor(
+ Profile::FromBrowserContext(context),
+ std::make_unique<DiceWebSigninInterceptorDelegate>());
+}
diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.h b/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.h
new file mode 100644
index 00000000000..ad6aac1ca3e
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.h
@@ -0,0 +1,37 @@
+// Copyright 2020 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_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class DiceWebSigninInterceptor;
+class Profile;
+
+class DiceWebSigninInterceptorFactory
+ : public BrowserContextKeyedServiceFactory {
+ public:
+ static DiceWebSigninInterceptor* GetForProfile(Profile* profile);
+ static DiceWebSigninInterceptorFactory* GetInstance();
+
+ DiceWebSigninInterceptorFactory(const DiceWebSigninInterceptorFactory&) =
+ delete;
+ DiceWebSigninInterceptorFactory& operator=(
+ const DiceWebSigninInterceptorFactory&) = delete;
+
+ private:
+ friend struct base::DefaultSingletonTraits<DiceWebSigninInterceptorFactory>;
+ DiceWebSigninInterceptorFactory();
+ ~DiceWebSigninInterceptorFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ void RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) override;
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor_unittest.cc b/chromium/chrome/browser/signin/dice_web_signin_interceptor_unittest.cc
new file mode 100644
index 00000000000..1f66d5d96ed
--- /dev/null
+++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor_unittest.cc
@@ -0,0 +1,953 @@
+// Copyright 2020 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.
+
+#include "chrome/browser/signin/dice_web_signin_interceptor.h"
+
+#include <memory>
+
+#include "base/callback.h"
+#include "base/memory/raw_ptr.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
+#include "build/buildflag.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/chrome_signin_client_test_util.h"
+#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/common/chrome_constants.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/browser_with_test_window_test.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "chrome/test/base/testing_profile.h"
+#include "chrome/test/base/testing_profile_manager.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/account_info.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "services/network/test/test_url_loader_factory.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "url/gurl.h"
+
+namespace {
+
+class MockDiceWebSigninInterceptorDelegate
+ : public DiceWebSigninInterceptor::Delegate {
+ public:
+ MOCK_METHOD(std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle>,
+ ShowSigninInterceptionBubble,
+ (content::WebContents * web_contents,
+ const BubbleParameters& bubble_parameters,
+ base::OnceCallback<void(SigninInterceptionResult)> callback),
+ (override));
+ void ShowProfileCustomizationBubble(Browser* browser) override {}
+};
+
+// Matches BubbleParameters fields excepting the color. This is useful in the
+// test because the color is randomly generated.
+testing::Matcher<const DiceWebSigninInterceptor::Delegate::BubbleParameters&>
+MatchBubbleParameters(
+ const DiceWebSigninInterceptor::Delegate::BubbleParameters& parameters) {
+ return testing::AllOf(
+ testing::Field("interception_type",
+ &DiceWebSigninInterceptor::Delegate::BubbleParameters::
+ interception_type,
+ parameters.interception_type),
+ testing::Field("intercepted_account",
+ &DiceWebSigninInterceptor::Delegate::BubbleParameters::
+ intercepted_account,
+ parameters.intercepted_account),
+ testing::Field("primary_account",
+ &DiceWebSigninInterceptor::Delegate::BubbleParameters::
+ primary_account,
+ parameters.primary_account));
+}
+
+// If the account info is valid, does nothing. Otherwise fills the extended
+// fields with default values.
+void MakeValidAccountInfo(AccountInfo* info) {
+ if (info->IsValid())
+ return;
+ info->full_name = "fullname";
+ info->given_name = "givenname";
+ info->hosted_domain = kNoHostedDomainFound;
+ info->locale = "en";
+ info->picture_url = "https://example.com";
+ DCHECK(info->IsValid());
+}
+
+} // namespace
+
+class DiceWebSigninInterceptorTest : public BrowserWithTestWindowTest {
+ public:
+ DiceWebSigninInterceptorTest() = default;
+ ~DiceWebSigninInterceptorTest() override = default;
+
+ DiceWebSigninInterceptor* interceptor() {
+ return dice_web_signin_interceptor_.get();
+ }
+
+ MockDiceWebSigninInterceptorDelegate* mock_delegate() {
+ return mock_delegate_;
+ }
+
+ content::WebContents* web_contents() {
+ return browser()->tab_strip_model()->GetActiveWebContents();
+ }
+
+ ProfileAttributesStorage* profile_attributes_storage() {
+ return profile_manager()->profile_attributes_storage();
+ }
+
+ signin::IdentityTestEnvironment* identity_test_env() {
+ return identity_test_env_profile_adaptor_->identity_test_env();
+ }
+
+ Profile* CreateTestingProfile(const std::string& name) {
+ return profile_manager()->CreateTestingProfile(name);
+ }
+
+ // Helper function that calls MaybeInterceptWebSignin with parameters
+ // compatible with interception.
+ void MaybeIntercept(CoreAccountId account_id) {
+ interceptor()->MaybeInterceptWebSignin(web_contents(), account_id,
+ /*is_new_account=*/true,
+ /*is_sync_signin=*/false);
+ }
+
+ // Calls MaybeInterceptWebSignin and verifies the heuristic outcome, the
+ // histograms and whether the interception is in progress.
+ // This function only works if the interception decision can be made
+ // synchronously (GetHeuristicOutcome() returns a value).
+ void TestSynchronousInterception(
+ AccountInfo account_info,
+ bool is_new_account,
+ bool is_sync_signin,
+ SigninInterceptionHeuristicOutcome expected_outcome) {
+ ASSERT_EQ(interceptor()->GetHeuristicOutcome(is_new_account, is_sync_signin,
+ account_info.email,
+ /*entry=*/nullptr),
+ expected_outcome);
+ base::HistogramTester histogram_tester;
+ interceptor()->MaybeInterceptWebSignin(web_contents(),
+ account_info.account_id,
+ is_new_account, is_sync_signin);
+ testing::Mock::VerifyAndClearExpectations(mock_delegate());
+ histogram_tester.ExpectUniqueSample("Signin.Intercept.HeuristicOutcome",
+ expected_outcome, 1);
+ EXPECT_EQ(interceptor()->is_interception_in_progress(),
+ SigninInterceptionHeuristicOutcomeIsSuccess(expected_outcome));
+ }
+
+ // Calls MaybeInterceptWebSignin and verifies the heuristic outcome and the
+ // histograms.
+ // This function only works if the interception decision cannot be made
+ // synchronously (GetHeuristicOutcome() returns no value).
+ void TestAsynchronousInterception(
+ AccountInfo account_info,
+ bool is_new_account,
+ bool is_sync_signin,
+ SigninInterceptionHeuristicOutcome expected_outcome) {
+ ASSERT_EQ(interceptor()->GetHeuristicOutcome(is_new_account, is_sync_signin,
+ account_info.email,
+ /*entry=*/nullptr),
+ absl::nullopt);
+ base::HistogramTester histogram_tester;
+ interceptor()->MaybeInterceptWebSignin(web_contents(),
+ account_info.account_id,
+ is_new_account, is_sync_signin);
+ testing::Mock::VerifyAndClearExpectations(mock_delegate());
+ histogram_tester.ExpectUniqueSample("Signin.Intercept.HeuristicOutcome",
+ expected_outcome, 1);
+ EXPECT_TRUE(interceptor()->is_interception_in_progress());
+ }
+
+ private:
+ // testing::Test:
+ void SetUp() override {
+ BrowserWithTestWindowTest::SetUp();
+
+ identity_test_env_profile_adaptor_ =
+ std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile());
+ identity_test_env_profile_adaptor_->identity_test_env()
+ ->SetTestURLLoaderFactory(&test_url_loader_factory_);
+
+ auto delegate = std::make_unique<
+ testing::StrictMock<MockDiceWebSigninInterceptorDelegate>>();
+ mock_delegate_ = delegate.get();
+ dice_web_signin_interceptor_ = std::make_unique<DiceWebSigninInterceptor>(
+ profile(), std::move(delegate));
+
+ // Create the first tab so that web_contents() exists.
+ AddTab(browser(), GURL("http://foo/1"));
+ }
+
+ void TearDown() override {
+ dice_web_signin_interceptor_->Shutdown();
+ identity_test_env_profile_adaptor_.reset();
+ BrowserWithTestWindowTest::TearDown();
+ }
+
+ TestingProfile::TestingFactories GetTestingFactories() override {
+ TestingProfile::TestingFactories factories =
+ IdentityTestEnvironmentProfileAdaptor::
+ GetIdentityTestEnvironmentFactories();
+ factories.push_back(
+ {ChromeSigninClientFactory::GetInstance(),
+ base::BindRepeating(&BuildChromeSigninClientWithURLLoader,
+ &test_url_loader_factory_)});
+ return factories;
+ }
+
+ network::TestURLLoaderFactory test_url_loader_factory_;
+ std::unique_ptr<IdentityTestEnvironmentProfileAdaptor>
+ identity_test_env_profile_adaptor_;
+ std::unique_ptr<DiceWebSigninInterceptor> dice_web_signin_interceptor_;
+ raw_ptr<MockDiceWebSigninInterceptorDelegate> mock_delegate_ = nullptr;
+};
+
+TEST_F(DiceWebSigninInterceptorTest, ShouldShowProfileSwitchBubble) {
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ const std::string& email = account_info.email;
+ EXPECT_FALSE(interceptor()->ShouldShowProfileSwitchBubble(
+ email, profile_attributes_storage()));
+
+ // Add another profile with no account.
+ CreateTestingProfile("Profile 1");
+ EXPECT_FALSE(interceptor()->ShouldShowProfileSwitchBubble(
+ email, profile_attributes_storage()));
+
+ // Add another profile with a different account.
+ Profile* profile_2 = CreateTestingProfile("Profile 2");
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile_2->GetPath());
+ ASSERT_NE(entry, nullptr);
+ std::string kOtherGaiaID = "SomeOtherGaiaID";
+ ASSERT_NE(kOtherGaiaID, account_info.gaia);
+ entry->SetAuthInfo(kOtherGaiaID, u"alice@gmail.com",
+ /*is_consented_primary_account=*/true);
+ EXPECT_FALSE(interceptor()->ShouldShowProfileSwitchBubble(
+ email, profile_attributes_storage()));
+
+ // Change the account to match.
+ entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email),
+ /*is_consented_primary_account=*/false);
+ const ProfileAttributesEntry* switch_to_entry =
+ interceptor()->ShouldShowProfileSwitchBubble(
+ email, profile_attributes_storage());
+ EXPECT_EQ(entry, switch_to_entry);
+}
+
+TEST_F(DiceWebSigninInterceptorTest, NoBubbleWithSingleAccount) {
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ MakeValidAccountInfo(&account_info);
+ account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ // Without UPA.
+ EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info));
+ EXPECT_FALSE(interceptor()->ShouldShowMultiUserBubble(account_info));
+
+ // With UPA.
+ identity_test_env()->SetPrimaryAccount("bob@example.com",
+ signin::ConsentLevel::kSignin);
+ EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info));
+}
+
+TEST_F(DiceWebSigninInterceptorTest, ShouldShowEnterpriseBubble) {
+ // Setup 3 accounts in the profile:
+ // - primary account
+ // - other enterprise account that is not primary (should be ignored)
+ // - intercepted account.
+ AccountInfo primary_account_info =
+ identity_test_env()->MakePrimaryAccountAvailable(
+ "alice@example.com", signin::ConsentLevel::kSignin);
+ AccountInfo other_account_info =
+ identity_test_env()->MakeAccountAvailable("dummy@example.com");
+ MakeValidAccountInfo(&other_account_info);
+ other_account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(other_account_info);
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ MakeValidAccountInfo(&account_info);
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ ASSERT_EQ(identity_test_env()->identity_manager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSignin),
+ primary_account_info.account_id);
+
+ // The primary account does not have full account info (empty domain).
+ ASSERT_TRUE(identity_test_env()
+ ->identity_manager()
+ ->FindExtendedAccountInfo(primary_account_info)
+ .hosted_domain.empty());
+ EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info));
+ account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ EXPECT_TRUE(interceptor()->ShouldShowEnterpriseBubble(account_info));
+
+ // The primary account has full info.
+ MakeValidAccountInfo(&primary_account_info);
+ identity_test_env()->UpdateAccountInfoForAccount(primary_account_info);
+ // The intercepted account is enterprise.
+ EXPECT_TRUE(interceptor()->ShouldShowEnterpriseBubble(account_info));
+ // Two consummer accounts.
+ account_info.hosted_domain = kNoHostedDomainFound;
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info));
+ // The primary account is enterprise.
+ primary_account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(primary_account_info);
+ EXPECT_TRUE(interceptor()->ShouldShowEnterpriseBubble(account_info));
+}
+
+class DiceWebSigninInterceptorForcedSeparationTest
+ : public DiceWebSigninInterceptorTest {
+ public:
+ DiceWebSigninInterceptorForcedSeparationTest()
+ : feature_list_(kAccountPoliciesLoadedWithoutSync) {}
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+};
+
+TEST_F(DiceWebSigninInterceptorForcedSeparationTest,
+ ShouldEnforceEnterpriseProfileSeparation) {
+ profile()->GetPrefs()->SetBoolean(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine, true);
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+
+ // Setup 3 accounts in the profile:
+ // - primary account
+ // - other enterprise account that is not primary (should be ignored)
+ // - intercepted account.
+ AccountInfo primary_account_info =
+ identity_test_env()->MakePrimaryAccountAvailable(
+ "alice@gmail.com", signin::ConsentLevel::kSignin);
+
+ AccountInfo other_account_info =
+ identity_test_env()->MakeAccountAvailable("dummy@example.com");
+ MakeValidAccountInfo(&other_account_info);
+ other_account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(other_account_info);
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ MakeValidAccountInfo(&account_info);
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ ASSERT_EQ(identity_test_env()->identity_manager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSignin),
+ primary_account_info.account_id);
+ interceptor()->new_account_interception_ = true;
+ // Consumer account not intercepted.
+ EXPECT_FALSE(
+ interceptor()->ShouldEnforceEnterpriseProfileSeparation(account_info));
+ account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ // Managed account intercepted.
+ EXPECT_TRUE(
+ interceptor()->ShouldEnforceEnterpriseProfileSeparation(account_info));
+}
+
+TEST_F(DiceWebSigninInterceptorForcedSeparationTest,
+ ShouldEnforceEnterpriseProfileSeparationWithoutUPA) {
+ profile()->GetPrefs()->SetBoolean(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine, true);
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+ AccountInfo account_info_1 =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ MakeValidAccountInfo(&account_info_1);
+ account_info_1.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info_1);
+
+ interceptor()->new_account_interception_ = true;
+ // Primary account is not set.
+ ASSERT_FALSE(identity_test_env()->identity_manager()->HasPrimaryAccount(
+ signin::ConsentLevel::kSignin));
+ EXPECT_TRUE(
+ interceptor()->ShouldEnforceEnterpriseProfileSeparation(account_info_1));
+}
+
+TEST_F(DiceWebSigninInterceptorForcedSeparationTest,
+ ShouldEnforceEnterpriseProfileSeparationReauth) {
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+ AccountInfo primary_account_info =
+ identity_test_env()->MakePrimaryAccountAvailable(
+ "alice@example.com", signin::ConsentLevel::kSignin);
+ MakeValidAccountInfo(&primary_account_info);
+ primary_account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(primary_account_info);
+
+ // Primary account is set.
+ ASSERT_TRUE(identity_test_env()->identity_manager()->HasPrimaryAccount(
+ signin::ConsentLevel::kSignin));
+ EXPECT_TRUE(interceptor()->ShouldEnforceEnterpriseProfileSeparation(
+ primary_account_info));
+
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile()->GetPath());
+ entry->SetUserAcceptedAccountManagement(true);
+
+ EXPECT_FALSE(interceptor()->ShouldEnforceEnterpriseProfileSeparation(
+ primary_account_info));
+}
+
+TEST_F(DiceWebSigninInterceptorForcedSeparationTest,
+ EnforceManagedAccountAsPrimaryReauth) {
+ profile()->GetPrefs()->SetBoolean(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine, true);
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account");
+
+ // Reauth intercepted if enterprise confirmation not shown yet for forced
+ // managed separation.
+ AccountInfo account_info = identity_test_env()->MakePrimaryAccountAvailable(
+ "alice@example.com", signin::ConsentLevel::kSignin);
+ MakeValidAccountInfo(&account_info);
+ account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account");
+
+ // Check that interception works otherwise, as a sanity check.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced,
+ account_info, account_info, SkColor()};
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+
+ TestAsynchronousInterception(
+ account_info, /*is_new_account=*/false, /*is_sync_signin=*/false,
+ SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced);
+}
+
+TEST_F(DiceWebSigninInterceptorForcedSeparationTest,
+ EnforceManagedAccountAsPrimaryManaged) {
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ MakeValidAccountInfo(&account_info);
+ account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+
+ // Check that interception works otherwise, as a sanity check.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced,
+ account_info, AccountInfo(), SkColor()};
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+ TestAsynchronousInterception(
+ account_info, /*is_new_account=*/true, /*is_sync_signin=*/false,
+ SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced);
+}
+
+TEST_F(DiceWebSigninInterceptorForcedSeparationTest,
+ EnforceManagedAccountAsPrimaryProfileSwitch) {
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ MakeValidAccountInfo(&account_info);
+ account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ profile()->GetPrefs()->SetBoolean(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine, true);
+ profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+
+ // Setup for profile switch interception.
+ Profile* profile_2 = CreateTestingProfile("Profile 2");
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile_2->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(account_info.email),
+ /*is_consented_primary_account=*/false);
+ // Check that interception works otherwise, as a sanity check.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitchForced,
+ account_info, AccountInfo(), SkColor()};
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+ TestAsynchronousInterception(account_info, /*is_new_account=*/true,
+ /*is_sync_signin=*/false,
+ SigninInterceptionHeuristicOutcome::
+ kInterceptEnterpriseForcedProfileSwitch);
+}
+
+TEST_F(DiceWebSigninInterceptorTest, ShouldShowEnterpriseBubbleWithoutUPA) {
+ AccountInfo account_info_1 =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ MakeValidAccountInfo(&account_info_1);
+ account_info_1.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info_1);
+ AccountInfo account_info_2 =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ MakeValidAccountInfo(&account_info_2);
+ account_info_2.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info_2);
+
+ // Primary account is not set.
+ ASSERT_FALSE(identity_test_env()->identity_manager()->HasPrimaryAccount(
+ signin::ConsentLevel::kSignin));
+ EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info_1));
+}
+
+TEST_F(DiceWebSigninInterceptorTest, ShouldShowMultiUserBubble) {
+ // Setup two accounts in the profile.
+ AccountInfo account_info_1 =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ MakeValidAccountInfo(&account_info_1);
+ account_info_1.given_name = "Bob";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info_1);
+ AccountInfo account_info_2 =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+
+ // The other account does not have full account info (empty name).
+ ASSERT_TRUE(account_info_2.given_name.empty());
+ EXPECT_TRUE(interceptor()->ShouldShowMultiUserBubble(account_info_1));
+
+ // Accounts with different names.
+ account_info_1.given_name = "Bob";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info_1);
+ MakeValidAccountInfo(&account_info_2);
+ account_info_2.given_name = "Alice";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info_2);
+ EXPECT_TRUE(interceptor()->ShouldShowMultiUserBubble(account_info_1));
+
+ // Accounts with same names.
+ account_info_1.given_name = "Alice";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info_1);
+ EXPECT_FALSE(interceptor()->ShouldShowMultiUserBubble(account_info_1));
+
+ // Comparison is case insensitive.
+ account_info_1.given_name = "alice";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info_1);
+ EXPECT_FALSE(interceptor()->ShouldShowMultiUserBubble(account_info_1));
+}
+
+TEST_F(DiceWebSigninInterceptorTest, NoInterception) {
+ // Setup for profile switch interception.
+ std::string email = "bob@example.com";
+ AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email);
+ Profile* profile_2 = CreateTestingProfile("Profile 2");
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile_2->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email),
+ /*is_consented_primary_account=*/false);
+
+ // Check that Sync signin is not intercepted.
+ TestSynchronousInterception(
+ account_info, /*is_new_account=*/true, /*is_sync_signin=*/true,
+ SigninInterceptionHeuristicOutcome::kAbortSyncSignin);
+
+ // Check that reauth is not intercepted.
+ TestSynchronousInterception(
+ account_info, /*is_new_account=*/false, /*is_sync_signin=*/false,
+ SigninInterceptionHeuristicOutcome::kAbortAccountNotNew);
+
+ // Check that interception works otherwise, as a sanity check.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch,
+ account_info, AccountInfo(), SkColor()};
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+ TestSynchronousInterception(
+ account_info, /*is_new_account=*/true, /*is_sync_signin=*/false,
+ SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch);
+}
+
+// Checks that the heuristic still works if the account was not added to Chrome
+// yet.
+TEST_F(DiceWebSigninInterceptorTest, HeuristicAccountNotAdded) {
+ // Setup for profile switch interception.
+ std::string email = "bob@example.com";
+ Profile* profile_2 = CreateTestingProfile("Profile 2");
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile_2->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->SetAuthInfo("dummy_gaia_id", base::UTF8ToUTF16(email),
+ /*is_consented_primary_account=*/false);
+ EXPECT_EQ(interceptor()->GetHeuristicOutcome(
+ /*is_new_account=*/true, /*is_sync_signin=*/false, email,
+ /*entry=*/nullptr),
+ SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch);
+}
+
+// Checks that the heuristic defaults to gmail.com when no domain is specified.
+TEST_F(DiceWebSigninInterceptorTest, HeuristicDefaultsToGmail) {
+ // Setup for profile switch interception.
+ std::string email = "bob@gmail.com";
+ Profile* profile_2 = CreateTestingProfile("Profile 2");
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile_2->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->SetAuthInfo("dummy_gaia_id", base::UTF8ToUTF16(email),
+ /*is_consented_primary_account=*/false);
+ // No domain defaults to gmail.com
+ EXPECT_EQ(interceptor()->GetHeuristicOutcome(
+ /*is_new_account=*/true, /*is_sync_signin=*/false, "bob",
+ /*entry=*/nullptr),
+ SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch);
+ // Using wrong domain does not trigger the interception.
+ EXPECT_EQ(
+ interceptor()->GetHeuristicOutcome(
+ /*is_new_account=*/true, /*is_sync_signin=*/false, "bob@example.com",
+ /*entry=*/nullptr),
+ SigninInterceptionHeuristicOutcome::kAbortSingleAccount);
+}
+
+// Checks that no heuristic is returned if signin interception is disabled.
+TEST_F(DiceWebSigninInterceptorTest, InterceptionDisabled) {
+ // Setup for profile switch interception.
+ std::string email = "bob@gmail.com";
+ Profile* profile_2 = CreateTestingProfile("Profile 2");
+ profile()->GetPrefs()->SetBoolean(prefs::kSigninInterceptionEnabled, false);
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile_2->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->SetAuthInfo("dummy_gaia_id", base::UTF8ToUTF16(email),
+ /*is_consented_primary_account=*/false);
+ EXPECT_EQ(interceptor()->GetHeuristicOutcome(
+ /*is_new_account=*/true, /*is_sync_signin=*/false, "bob",
+ /*entry=*/nullptr),
+ SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled);
+ EXPECT_EQ(
+ interceptor()->GetHeuristicOutcome(
+ /*is_new_account=*/true, /*is_sync_signin=*/false, "bob@example.com",
+ /*entry=*/nullptr),
+ SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled);
+}
+
+TEST_F(DiceWebSigninInterceptorTest, TabClosed) {
+ base::HistogramTester histogram_tester;
+ interceptor()->MaybeInterceptWebSignin(
+ /*web_contents=*/nullptr, CoreAccountId(),
+ /*is_new_account=*/true, /*is_sync_signin=*/false);
+ histogram_tester.ExpectUniqueSample(
+ "Signin.Intercept.HeuristicOutcome",
+ SigninInterceptionHeuristicOutcome::kAbortTabClosed, 1);
+}
+
+TEST_F(DiceWebSigninInterceptorTest, InterceptionInProgress) {
+ // Setup for profile switch interception.
+ std::string email = "bob@example.com";
+ AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email);
+ Profile* profile_2 = CreateTestingProfile("Profile 2");
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile_2->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email),
+ /*is_consented_primary_account=*/false);
+
+ // Start an interception.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch,
+ account_info, AccountInfo(), SkColor()};
+ base::OnceCallback<void(SigninInterceptionResult)> delegate_callback;
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_))
+ .WillOnce(testing::WithArg<2>(testing::Invoke(
+ [&delegate_callback](
+ base::OnceCallback<void(SigninInterceptionResult)> callback) {
+ delegate_callback = std::move(callback);
+ return nullptr;
+ })));
+ MaybeIntercept(account_info.account_id);
+ testing::Mock::VerifyAndClearExpectations(mock_delegate());
+ EXPECT_TRUE(interceptor()->is_interception_in_progress());
+
+ // Check that there is no interception while another one is in progress.
+ base::HistogramTester histogram_tester;
+ MaybeIntercept(account_info.account_id);
+ testing::Mock::VerifyAndClearExpectations(mock_delegate());
+ histogram_tester.ExpectUniqueSample(
+ "Signin.Intercept.HeuristicOutcome",
+ SigninInterceptionHeuristicOutcome::kAbortInterceptInProgress, 1);
+
+ // Complete the interception that was in progress.
+ std::move(delegate_callback).Run(SigninInterceptionResult::kDeclined);
+ EXPECT_FALSE(interceptor()->is_interception_in_progress());
+
+ // A new interception can now start.
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+ MaybeIntercept(account_info.account_id);
+}
+
+TEST_F(DiceWebSigninInterceptorTest, DeclineCreationRepeatedly) {
+ base::HistogramTester histogram_tester;
+ AccountInfo primary_account_info =
+ identity_test_env()->MakePrimaryAccountAvailable(
+ "bob@example.com", signin::ConsentLevel::kSignin);
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ MakeValidAccountInfo(&account_info);
+ account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ const int kMaxProfileCreationDeclinedCount = 2;
+ // Decline the interception kMaxProfileCreationDeclinedCount times.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise,
+ account_info, primary_account_info, SkColor()};
+ for (int i = 0; i < kMaxProfileCreationDeclinedCount; ++i) {
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_))
+ .WillOnce(testing::WithArg<2>(testing::Invoke(
+ [](base::OnceCallback<void(SigninInterceptionResult)> callback) {
+ std::move(callback).Run(SigninInterceptionResult::kDeclined);
+ return nullptr;
+ })));
+ MaybeIntercept(account_info.account_id);
+ EXPECT_EQ(interceptor()->is_interception_in_progress(), false);
+ histogram_tester.ExpectUniqueSample(
+ "Signin.Intercept.HeuristicOutcome",
+ SigninInterceptionHeuristicOutcome::kInterceptEnterprise, i + 1);
+ }
+
+ // Next time the interception is not shown again.
+ MaybeIntercept(account_info.account_id);
+ EXPECT_EQ(interceptor()->is_interception_in_progress(), false);
+ histogram_tester.ExpectBucketCount(
+ "Signin.Intercept.HeuristicOutcome",
+ SigninInterceptionHeuristicOutcome::kAbortUserDeclinedProfileForAccount,
+ 1);
+
+ // Another account can still be intercepted.
+ account_info.email = "oscar@example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise,
+ account_info, primary_account_info, SkColor()};
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+ MaybeIntercept(account_info.account_id);
+ histogram_tester.ExpectBucketCount(
+ "Signin.Intercept.HeuristicOutcome",
+ SigninInterceptionHeuristicOutcome::kInterceptEnterprise,
+ kMaxProfileCreationDeclinedCount + 1);
+ EXPECT_EQ(interceptor()->is_interception_in_progress(), true);
+}
+
+TEST_F(DiceWebSigninInterceptorTest, DeclineSwitchRepeatedly_NoLimit) {
+ base::HistogramTester histogram_tester;
+ // Setup for profile switch interception.
+ std::string email = "bob@example.com";
+ AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email);
+ Profile* profile_2 = CreateTestingProfile("Profile 2");
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile_2->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email),
+ /*is_consented_primary_account=*/false);
+
+ // Test that the profile switch can be declined multiple times.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch,
+ account_info, AccountInfo(), SkColor()};
+ for (int i = 0; i < 10; ++i) {
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_))
+ .WillOnce(testing::WithArg<2>(testing::Invoke(
+ [](base::OnceCallback<void(SigninInterceptionResult)> callback) {
+ std::move(callback).Run(SigninInterceptionResult::kDeclined);
+ return nullptr;
+ })));
+ MaybeIntercept(account_info.account_id);
+ EXPECT_EQ(interceptor()->is_interception_in_progress(), false);
+ histogram_tester.ExpectUniqueSample(
+ "Signin.Intercept.HeuristicOutcome",
+ SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch, i + 1);
+ }
+}
+
+TEST_F(DiceWebSigninInterceptorTest, PersistentHash) {
+ // The hash is persistent (the value should never change).
+ EXPECT_EQ("email_174",
+ interceptor()->GetPersistentEmailHash("alice@example.com"));
+ // Different email get another hash.
+ EXPECT_NE(interceptor()->GetPersistentEmailHash("bob@gmail.com"),
+ interceptor()->GetPersistentEmailHash("alice@example.com"));
+ // Equivalent emails get the same hash.
+ EXPECT_EQ(interceptor()->GetPersistentEmailHash("bob"),
+ interceptor()->GetPersistentEmailHash("bob@gmail.com"));
+ EXPECT_EQ(interceptor()->GetPersistentEmailHash("bo.b@gmail.com"),
+ interceptor()->GetPersistentEmailHash("bob@gmail.com"));
+ // Dots are removed only for gmail accounts.
+ EXPECT_NE(interceptor()->GetPersistentEmailHash("alice@example.com"),
+ interceptor()->GetPersistentEmailHash("al.ice@example.com"));
+}
+
+// Interception other than the profile switch require at least 2 accounts.
+TEST_F(DiceWebSigninInterceptorTest, NoInterceptionWithOneAccount) {
+ base::HistogramTester histogram_tester;
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("bob@example.com");
+ // Interception aborts even if the account info is not available.
+ ASSERT_FALSE(identity_test_env()
+ ->identity_manager()
+ ->FindExtendedAccountInfoByAccountId(account_info.account_id)
+ .IsValid());
+ TestSynchronousInterception(
+ account_info, /*is_new_account=*/true, /*is_sync_signin=*/false,
+ SigninInterceptionHeuristicOutcome::kAbortSingleAccount);
+}
+
+// When profile creation is disallowed, profile switch interception is still
+// enabled, but others are disabled.
+TEST_F(DiceWebSigninInterceptorTest, ProfileCreationDisallowed) {
+ base::HistogramTester histogram_tester;
+ g_browser_process->local_state()->SetBoolean(prefs::kBrowserAddPersonEnabled,
+ false);
+ // Setup for profile switch interception.
+ std::string email = "bob@example.com";
+ AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email);
+ AccountInfo other_account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ Profile* profile_2 = CreateTestingProfile("Profile 2");
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage()->GetProfileAttributesWithPath(
+ profile_2->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email),
+ /*is_consented_primary_account=*/false);
+
+ // Interception that would offer creating a new profile does not work.
+ TestSynchronousInterception(
+ other_account_info, /*is_new_account=*/true, /*is_sync_signin=*/false,
+ SigninInterceptionHeuristicOutcome::kAbortProfileCreationDisallowed);
+
+ // Profile switch interception still works.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch,
+ account_info, AccountInfo(), SkColor()};
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+ MaybeIntercept(account_info.account_id);
+}
+
+TEST_F(DiceWebSigninInterceptorTest, WaitForAccountInfoAvailable) {
+ base::HistogramTester histogram_tester;
+ AccountInfo primary_account_info =
+ identity_test_env()->MakePrimaryAccountAvailable(
+ "bob@example.com", signin::ConsentLevel::kSignin);
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ EXPECT_FALSE(interceptor()
+ ->GetHeuristicOutcome(/*is_new_account=*/true,
+ /*is_sync_signin=*/false,
+ account_info.email,
+ /*entry=*/nullptr)
+ .has_value());
+ MaybeIntercept(account_info.account_id);
+ // Delegate was not called yet.
+ testing::Mock::VerifyAndClearExpectations(mock_delegate());
+
+ // Account info becomes available, interception happens.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise,
+ account_info, primary_account_info, SkColor()};
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+ MakeValidAccountInfo(&account_info);
+ account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+ histogram_tester.ExpectTotalCount("Signin.Intercept.AccountInfoFetchDuration",
+ 1);
+}
+
+TEST_F(DiceWebSigninInterceptorTest, AccountInfoAlreadyAvailable) {
+ base::HistogramTester histogram_tester;
+ AccountInfo primary_account_info =
+ identity_test_env()->MakePrimaryAccountAvailable(
+ "bob@example.com", signin::ConsentLevel::kSignin);
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ MakeValidAccountInfo(&account_info);
+ account_info.hosted_domain = "example.com";
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ // Account info is already available, interception happens immediately.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise,
+ account_info, primary_account_info, SkColor()};
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+ MaybeIntercept(account_info.account_id);
+ histogram_tester.ExpectTotalCount("Signin.Intercept.AccountInfoFetchDuration",
+ 1);
+ histogram_tester.ExpectUniqueSample(
+ "Signin.Intercept.HeuristicOutcome",
+ SigninInterceptionHeuristicOutcome::kInterceptEnterprise, 1);
+}
+
+TEST_F(DiceWebSigninInterceptorTest, MultiUserInterception) {
+ base::HistogramTester histogram_tester;
+ AccountInfo primary_account_info =
+ identity_test_env()->MakePrimaryAccountAvailable(
+ "bob@example.com", signin::ConsentLevel::kSignin);
+ AccountInfo account_info =
+ identity_test_env()->MakeAccountAvailable("alice@example.com");
+ MakeValidAccountInfo(&account_info);
+ identity_test_env()->UpdateAccountInfoForAccount(account_info);
+
+ // Account info is already available, interception happens immediately.
+ DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = {
+ DiceWebSigninInterceptor::SigninInterceptionType::kMultiUser,
+ account_info, primary_account_info, SkColor()};
+ EXPECT_CALL(*mock_delegate(),
+ ShowSigninInterceptionBubble(
+ web_contents(), MatchBubbleParameters(expected_parameters),
+ testing::_));
+ MaybeIntercept(account_info.account_id);
+ histogram_tester.ExpectUniqueSample(
+ "Signin.Intercept.HeuristicOutcome",
+ SigninInterceptionHeuristicOutcome::kInterceptMultiUser, 1);
+}
diff --git a/chromium/chrome/browser/signin/e2e_tests/live_sign_in_test.cc b/chromium/chrome/browser/signin/e2e_tests/live_sign_in_test.cc
new file mode 100644
index 00000000000..3e042e76b87
--- /dev/null
+++ b/chromium/chrome/browser/signin/e2e_tests/live_sign_in_test.cc
@@ -0,0 +1,776 @@
+// Copyright 2019 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.
+
+#include "base/memory/raw_ptr.h"
+#include "base/run_loop.h"
+#include "base/scoped_observation.h"
+#include "base/strings/stringprintf.h"
+#include "base/time/time.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/signin/account_reconcilor_factory.h"
+#include "chrome/browser/signin/e2e_tests/live_test.h"
+#include "chrome/browser/signin/e2e_tests/test_accounts_util.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/sync/sync_service_factory.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
+#include "chrome/browser/ui/webui/signin/login_ui_test_utils.h"
+#include "chrome/browser/ui/webui/signin/signin_email_confirmation_dialog.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/signin/core/browser/account_reconcilor.h"
+#include "components/signin/public/base/consent_level.h"
+#include "components/signin/public/identity_manager/account_capabilities.h"
+#include "components/signin/public/identity_manager/account_info.h"
+#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "components/signin/public/identity_manager/tribool.h"
+#include "components/sync/driver/sync_service.h"
+#include "content/public/test/browser_test.h"
+#include "google_apis/gaia/core_account_id.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+#include "google_apis/gaia/gaia_urls.h"
+#include "ui/compositor/scoped_animation_duration_scale_mode.h"
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/sync/sync_ui_util.h"
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
+
+namespace signin {
+namespace test {
+
+const base::TimeDelta kDialogTimeout = base::Seconds(10);
+
+// A wrapper importing the settings module when the chrome://settings serve the
+// Polymer 3 version.
+const char kSettingsScriptWrapperFormat[] =
+ "import('./settings.js').then(settings => {%s});";
+
+enum class PrimarySyncAccountWait { kWaitForAdded, kWaitForCleared, kNotWait };
+
+// Observes various sign-in events and allows to wait for a specific state of
+// signed-in accounts.
+class SignInTestObserver : public IdentityManager::Observer,
+ public AccountReconcilor::Observer {
+ public:
+ explicit SignInTestObserver(IdentityManager* identity_manager,
+ AccountReconcilor* reconcilor)
+ : identity_manager_(identity_manager), reconcilor_(reconcilor) {
+ identity_manager_observation_.Observe(identity_manager_.get());
+ account_reconcilor_observation_.Observe(reconcilor_.get());
+ }
+ ~SignInTestObserver() override = default;
+
+ // IdentityManager::Observer:
+ void OnPrimaryAccountChanged(
+ const PrimaryAccountChangeEvent& event) override {
+ if (event.GetEventTypeFor(ConsentLevel::kSync) ==
+ PrimaryAccountChangeEvent::Type::kNone) {
+ return;
+ }
+ QuitIfConditionIsSatisfied();
+ }
+ void OnRefreshTokenUpdatedForAccount(const CoreAccountInfo&) override {
+ QuitIfConditionIsSatisfied();
+ }
+ void OnRefreshTokenRemovedForAccount(const CoreAccountId&) override {
+ QuitIfConditionIsSatisfied();
+ }
+ void OnErrorStateOfRefreshTokenUpdatedForAccount(
+ const CoreAccountInfo&,
+ const GoogleServiceAuthError&) override {
+ QuitIfConditionIsSatisfied();
+ }
+ void OnAccountsInCookieUpdated(const AccountsInCookieJarInfo&,
+ const GoogleServiceAuthError&) override {
+ QuitIfConditionIsSatisfied();
+ }
+
+ // AccountReconcilor::Observer:
+ // TODO(https://crbug.com/1051864): Remove this obsever method once the bug is
+ // fixed.
+ void OnStateChanged(signin_metrics::AccountReconcilorState state) override {
+ if (state == signin_metrics::ACCOUNT_RECONCILOR_OK) {
+ // This will trigger cookie update if accounts are stale.
+ identity_manager_->GetAccountsInCookieJar();
+ }
+ }
+
+ void WaitForAccountChanges(int signed_in_accounts,
+ PrimarySyncAccountWait primary_sync_account_wait) {
+ expected_signed_in_accounts_ = signed_in_accounts;
+ primary_sync_account_wait_ = primary_sync_account_wait;
+ are_expectations_set = true;
+ QuitIfConditionIsSatisfied();
+ run_loop_.Run();
+ }
+
+ private:
+ void QuitIfConditionIsSatisfied() {
+ if (!are_expectations_set)
+ return;
+
+ int accounts_with_valid_refresh_token =
+ CountAccountsWithValidRefreshToken();
+ int accounts_in_cookie = CountSignedInAccountsInCookie();
+
+ if (accounts_with_valid_refresh_token != accounts_in_cookie ||
+ accounts_with_valid_refresh_token != expected_signed_in_accounts_) {
+ return;
+ }
+
+ switch (primary_sync_account_wait_) {
+ case PrimarySyncAccountWait::kWaitForAdded:
+ if (!HasValidPrimarySyncAccount())
+ return;
+ break;
+ case PrimarySyncAccountWait::kWaitForCleared:
+ if (identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync))
+ return;
+ break;
+ case PrimarySyncAccountWait::kNotWait:
+ break;
+ }
+
+ run_loop_.Quit();
+ }
+
+ int CountAccountsWithValidRefreshToken() const {
+ std::vector<CoreAccountInfo> accounts_with_refresh_tokens =
+ identity_manager_->GetAccountsWithRefreshTokens();
+ int valid_accounts = 0;
+ for (const auto& account_info : accounts_with_refresh_tokens) {
+ if (!identity_manager_->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_info.account_id)) {
+ ++valid_accounts;
+ }
+ }
+ return valid_accounts;
+ }
+
+ int CountSignedInAccountsInCookie() const {
+ signin::AccountsInCookieJarInfo accounts_in_cookie_jar =
+ identity_manager_->GetAccountsInCookieJar();
+ if (!accounts_in_cookie_jar.accounts_are_fresh)
+ return -1;
+
+ return accounts_in_cookie_jar.signed_in_accounts.size();
+ }
+
+ bool HasValidPrimarySyncAccount() const {
+ CoreAccountId primary_account_id =
+ identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSync);
+ if (primary_account_id.empty())
+ return false;
+
+ return !identity_manager_->HasAccountWithRefreshTokenInPersistentErrorState(
+ primary_account_id);
+ }
+
+ const raw_ptr<signin::IdentityManager> identity_manager_;
+ const raw_ptr<AccountReconcilor> reconcilor_;
+ base::ScopedObservation<IdentityManager, IdentityManager::Observer>
+ identity_manager_observation_{this};
+ base::ScopedObservation<AccountReconcilor, AccountReconcilor::Observer>
+ account_reconcilor_observation_{this};
+ base::RunLoop run_loop_;
+
+ bool are_expectations_set = false;
+ int expected_signed_in_accounts_ = 0;
+ PrimarySyncAccountWait primary_sync_account_wait_ =
+ PrimarySyncAccountWait::kNotWait;
+};
+
+// Observer class allowing to wait for account capabilities to be known.
+class AccountCapabilitiesObserver : public IdentityManager::Observer {
+ public:
+ explicit AccountCapabilitiesObserver(IdentityManager* identity_manager)
+ : identity_manager_(identity_manager) {
+ identity_manager_observation_.Observe(identity_manager);
+ }
+
+ // IdentityManager::Observer:
+ void OnExtendedAccountInfoUpdated(const AccountInfo& info) override {
+ if (info.account_id != account_id_)
+ return;
+
+ if (info.capabilities.AreAllCapabilitiesKnown())
+ run_loop_.Quit();
+ }
+
+ // This should be called only once per AccountCapabilitiesObserver instance.
+ void WaitForAllCapabilitiesToBeKnown(CoreAccountId account_id) {
+ DCHECK(identity_manager_observation_.IsObservingSource(
+ identity_manager_.get()));
+ AccountInfo info =
+ identity_manager_->FindExtendedAccountInfoByAccountId(account_id);
+ if (info.capabilities.AreAllCapabilitiesKnown())
+ return;
+
+ account_id_ = account_id;
+ run_loop_.Run();
+ identity_manager_observation_.Reset();
+ }
+
+ private:
+ raw_ptr<IdentityManager> identity_manager_ = nullptr;
+ CoreAccountId account_id_;
+ base::RunLoop run_loop_;
+ base::ScopedObservation<IdentityManager, IdentityManager::Observer>
+ identity_manager_observation_{this};
+};
+
+// Live tests for SignIn.
+// These tests can be run with:
+// browser_tests --gtest_filter=LiveSignInTest.* --run-live-tests --run-manual
+class LiveSignInTest : public signin::test::LiveTest {
+ public:
+ LiveSignInTest() = default;
+ ~LiveSignInTest() override = default;
+
+ void SetUp() override {
+ LiveTest::SetUp();
+ // Always disable animation for stability.
+ ui::ScopedAnimationDurationScaleMode disable_animation(
+ ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
+ }
+
+ void SignInFromWeb(const TestAccount& test_account,
+ int previously_signed_in_accounts) {
+ AddTabAtIndex(0, GaiaUrls::GetInstance()->add_account_url(),
+ ui::PageTransition::PAGE_TRANSITION_TYPED);
+ SignInFromCurrentPage(test_account, previously_signed_in_accounts);
+ }
+
+ void SignInFromSettings(const TestAccount& test_account,
+ int previously_signed_in_accounts) {
+ GURL settings_url("chrome://settings");
+ AddTabAtIndex(0, settings_url, ui::PageTransition::PAGE_TRANSITION_TYPED);
+ auto* settings_tab = browser()->tab_strip_model()->GetActiveWebContents();
+ EXPECT_TRUE(content::ExecuteScript(
+ settings_tab,
+ base::StringPrintf(
+ kSettingsScriptWrapperFormat,
+ "settings.SyncBrowserProxyImpl.getInstance().startSignIn();")));
+ SignInFromCurrentPage(test_account, previously_signed_in_accounts);
+ }
+
+ void SignInFromCurrentPage(const TestAccount& test_account,
+ int previously_signed_in_accounts) {
+ SignInTestObserver observer(identity_manager(), account_reconcilor());
+ login_ui_test_utils::ExecuteJsToSigninInSigninFrame(
+ browser(), test_account.user, test_account.password);
+ observer.WaitForAccountChanges(previously_signed_in_accounts + 1,
+ PrimarySyncAccountWait::kNotWait);
+ }
+
+ void TurnOnSync(const TestAccount& test_account,
+ int previously_signed_in_accounts) {
+ SignInFromSettings(test_account, previously_signed_in_accounts);
+
+ SignInTestObserver observer(identity_manager(), account_reconcilor());
+ EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog(
+ browser(), kDialogTimeout));
+ observer.WaitForAccountChanges(previously_signed_in_accounts + 1,
+ PrimarySyncAccountWait::kWaitForAdded);
+ }
+
+ void SignOutFromWeb() {
+ SignInTestObserver observer(identity_manager(), account_reconcilor());
+ AddTabAtIndex(0, GaiaUrls::GetInstance()->service_logout_url(),
+ ui::PageTransition::PAGE_TRANSITION_TYPED);
+ observer.WaitForAccountChanges(0, PrimarySyncAccountWait::kNotWait);
+ }
+
+ void TurnOffSync() {
+ GURL settings_url("chrome://settings");
+ AddTabAtIndex(0, settings_url, ui::PageTransition::PAGE_TRANSITION_TYPED);
+ SignInTestObserver observer(identity_manager(), account_reconcilor());
+ auto* settings_tab = browser()->tab_strip_model()->GetActiveWebContents();
+ EXPECT_TRUE(content::ExecuteScript(
+ settings_tab,
+ base::StringPrintf(
+ kSettingsScriptWrapperFormat,
+ "settings.SyncBrowserProxyImpl.getInstance().signOut(false)")));
+ observer.WaitForAccountChanges(0, PrimarySyncAccountWait::kWaitForCleared);
+ }
+
+ signin::IdentityManager* identity_manager() {
+ return identity_manager(browser());
+ }
+
+ signin::IdentityManager* identity_manager(Browser* browser) {
+ return IdentityManagerFactory::GetForProfile(browser->profile());
+ }
+
+ syncer::SyncService* sync_service() { return sync_service(browser()); }
+
+ syncer::SyncService* sync_service(Browser* browser) {
+ return SyncServiceFactory::GetForProfile(browser->profile());
+ }
+
+ AccountReconcilor* account_reconcilor() {
+ return account_reconcilor(browser());
+ }
+
+ AccountReconcilor* account_reconcilor(Browser* browser) {
+ return AccountReconcilorFactory::GetForProfile(browser->profile());
+ }
+};
+
+// This test can pass. Marked as manual because it TIMED_OUT on Win7.
+// See crbug.com/1025335.
+// Sings in an account through the settings page and checks that the account is
+// added to Chrome. Sync should be disabled because the test doesn't pass
+// through the Sync confirmation dialog.
+IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_SimpleSignInFlow) {
+ TestAccount ta;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", ta));
+ SignInFromSettings(ta, 0);
+
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh);
+ ASSERT_EQ(1u, accounts_in_cookie_jar.signed_in_accounts.size());
+ EXPECT_TRUE(accounts_in_cookie_jar.signed_out_accounts.empty());
+ const gaia::ListedAccount& account =
+ accounts_in_cookie_jar.signed_in_accounts[0];
+ EXPECT_TRUE(gaia::AreEmailsSame(ta.user, account.email));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account.id));
+ EXPECT_FALSE(sync_service()->IsSyncFeatureEnabled());
+}
+
+// This test can pass. Marked as manual because it TIMED_OUT on Win7.
+// See crbug.com/1025335.
+// Signs in an account through the settings page and enables Sync. Checks that
+// Sync is enabled.
+// Then, signs out on the web and checks that the account is removed from
+// cookies and Sync paused error is displayed.
+IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_WebSignOut) {
+ TestAccount test_account;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account));
+ TurnOnSync(test_account, 0);
+
+ const CoreAccountInfo& primary_account =
+ identity_manager()->GetPrimaryAccountInfo(signin::ConsentLevel::kSync);
+ EXPECT_FALSE(primary_account.IsEmpty());
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account.user, primary_account.email));
+ EXPECT_TRUE(sync_service()->IsSyncFeatureEnabled());
+
+ SignOutFromWeb();
+
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh);
+ ASSERT_TRUE(accounts_in_cookie_jar.signed_in_accounts.empty());
+ ASSERT_EQ(1u, accounts_in_cookie_jar.signed_out_accounts.size());
+ EXPECT_TRUE(gaia::AreEmailsSame(
+ test_account.user, accounts_in_cookie_jar.signed_out_accounts[0].email));
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ primary_account.account_id));
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+ EXPECT_EQ(GetAvatarSyncErrorType(browser()->profile()),
+ AvatarSyncErrorType::kAuthError);
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
+}
+
+// This test can pass. Marked as manual because it TIMED_OUT on Win7.
+// See crbug.com/1025335.
+// Sings in two accounts on the web and checks that cookies and refresh tokens
+// are added to Chrome. Sync should be disabled.
+// Then, signs out on the web and checks that accounts are removed from Chrome.
+IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_WebSignInAndSignOut) {
+ TestAccount test_account_1;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1));
+ SignInFromWeb(test_account_1, 0);
+
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar_1 =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar_1.accounts_are_fresh);
+ ASSERT_EQ(1u, accounts_in_cookie_jar_1.signed_in_accounts.size());
+ EXPECT_TRUE(accounts_in_cookie_jar_1.signed_out_accounts.empty());
+ const gaia::ListedAccount& account_1 =
+ accounts_in_cookie_jar_1.signed_in_accounts[0];
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account_1.user, account_1.email));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_1.id));
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+
+ TestAccount test_account_2;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2));
+ SignInFromWeb(test_account_2, 1);
+
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar_2 =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar_2.accounts_are_fresh);
+ ASSERT_EQ(2u, accounts_in_cookie_jar_2.signed_in_accounts.size());
+ EXPECT_TRUE(accounts_in_cookie_jar_2.signed_out_accounts.empty());
+ EXPECT_EQ(accounts_in_cookie_jar_2.signed_in_accounts[0].id, account_1.id);
+ const gaia::ListedAccount& account_2 =
+ accounts_in_cookie_jar_2.signed_in_accounts[1];
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, account_2.email));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_2.id));
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+
+ SignOutFromWeb();
+
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar_3 =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar_3.accounts_are_fresh);
+ ASSERT_TRUE(accounts_in_cookie_jar_3.signed_in_accounts.empty());
+ EXPECT_EQ(2u, accounts_in_cookie_jar_3.signed_out_accounts.size());
+ EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty());
+}
+
+// This test can pass. Marked as manual because it TIMED_OUT on Win7.
+// See crbug.com/1025335.
+// Signs in an account through the settings page and enables Sync. Checks that
+// Sync is enabled. Signs in a second account on the web.
+// Then, turns Sync off from the settings page and checks that both accounts are
+// removed from Chrome and from cookies.
+IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_TurnOffSync) {
+ TestAccount test_account_1;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1));
+ TurnOnSync(test_account_1, 0);
+
+ TestAccount test_account_2;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2));
+ SignInFromWeb(test_account_2, 1);
+
+ const CoreAccountInfo& primary_account =
+ identity_manager()->GetPrimaryAccountInfo(signin::ConsentLevel::kSync);
+ EXPECT_FALSE(primary_account.IsEmpty());
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account_1.user, primary_account.email));
+ EXPECT_TRUE(sync_service()->IsSyncFeatureEnabled());
+
+ TurnOffSync();
+
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar_2 =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar_2.accounts_are_fresh);
+ ASSERT_TRUE(accounts_in_cookie_jar_2.signed_in_accounts.empty());
+ EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty());
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+}
+
+// In "Sync paused" state, when the primary account is invalid, turns off sync
+// from settings. Checks that the account is removed from Chrome.
+// Regression test for https://crbug.com/1114646
+IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_TurnOffSyncWhenPaused) {
+ TestAccount test_account_1;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1));
+ TurnOnSync(test_account_1, 0);
+
+ // Get in sync paused state.
+ SignOutFromWeb();
+
+ const CoreAccountInfo& primary_account =
+ identity_manager()->GetPrimaryAccountInfo(signin::ConsentLevel::kSync);
+ EXPECT_FALSE(primary_account.IsEmpty());
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account_1.user, primary_account.email));
+ EXPECT_TRUE(sync_service()->IsSyncFeatureEnabled());
+ EXPECT_TRUE(
+ identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
+ primary_account.account_id));
+
+ TurnOffSync();
+ EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty());
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+}
+
+// This test can pass. Marked as manual because it TIMED_OUT on Win7.
+// See crbug.com/1025335.
+// Signs in an account on the web. Goes to the Chrome settings to enable Sync
+// but cancels the sync confirmation dialog. Checks that the account is still
+// signed in on the web but Sync is disabled.
+IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_CancelSyncWithWebAccount) {
+ TestAccount test_account;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account));
+ SignInFromWeb(test_account, 0);
+
+ SignInTestObserver observer(identity_manager(), account_reconcilor());
+ GURL settings_url("chrome://settings");
+ AddTabAtIndex(0, settings_url, ui::PageTransition::PAGE_TRANSITION_TYPED);
+ auto* settings_tab = browser()->tab_strip_model()->GetActiveWebContents();
+ std::string start_syncing_script = base::StringPrintf(
+ "settings.SyncBrowserProxyImpl.getInstance()."
+ "startSyncingWithEmail(\"%s\", true);",
+ test_account.user.c_str());
+ EXPECT_TRUE(content::ExecuteScript(
+ settings_tab, base::StringPrintf(kSettingsScriptWrapperFormat,
+ start_syncing_script.c_str())));
+ EXPECT_TRUE(login_ui_test_utils::CancelSyncConfirmationDialog(
+ browser(), kDialogTimeout));
+ observer.WaitForAccountChanges(1, PrimarySyncAccountWait::kWaitForCleared);
+
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh);
+ ASSERT_EQ(1u, accounts_in_cookie_jar.signed_in_accounts.size());
+ const gaia::ListedAccount& account =
+ accounts_in_cookie_jar.signed_in_accounts[0];
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account.user, account.email));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account.id));
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+}
+
+// This test can pass. Marked as manual because it TIMED_OUT on Win7.
+// See crbug.com/1025335.
+// Starts the sign in flow from the settings page, enters credentials on the
+// login page but cancels the Sync confirmation dialog. Checks that Sync is
+// disabled and no account was added to Chrome.
+IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_CancelSync) {
+ TestAccount test_account;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account));
+ SignInFromSettings(test_account, 0);
+
+ SignInTestObserver observer(identity_manager(), account_reconcilor());
+ EXPECT_TRUE(login_ui_test_utils::CancelSyncConfirmationDialog(
+ browser(), kDialogTimeout));
+ observer.WaitForAccountChanges(0, PrimarySyncAccountWait::kWaitForCleared);
+
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh);
+ EXPECT_TRUE(accounts_in_cookie_jar.signed_in_accounts.empty());
+ EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty());
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+}
+
+// This test can pass. Marked as manual because it TIMED_OUT on Win7.
+// See crbug.com/1025335.
+// Enables and disables sync to account 1. Enables sync to account 2 and clicks
+// on "This wasn't me" in the email confirmation dialog. Checks that the new
+// profile is created. Checks that Sync to account 2 is enabled in the new
+// profile. Checks that account 2 was removed from the original profile.
+IN_PROC_BROWSER_TEST_F(LiveSignInTest,
+ MANUAL_SyncSecondAccount_CreateNewProfile) {
+ // Enable and disable sync for the first account.
+ TestAccount test_account_1;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1));
+ TurnOnSync(test_account_1, 0);
+ TurnOffSync();
+
+ // Start enable sync for the second account.
+ TestAccount test_account_2;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2));
+ SignInFromSettings(test_account_2, 0);
+
+ // Set up an observer for removing the second account from the original
+ // profile.
+ SignInTestObserver original_browser_observer(identity_manager(),
+ account_reconcilor());
+
+ // Check there is only one profile.
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U);
+ EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U);
+
+ // Click "This wasn't me" on the email confirmation dialog and wait for a new
+ // browser and profile created.
+ EXPECT_TRUE(login_ui_test_utils::CompleteSigninEmailConfirmationDialog(
+ browser(), kDialogTimeout,
+ SigninEmailConfirmationDialog::CREATE_NEW_USER));
+ Browser* new_browser = ui_test_utils::WaitForBrowserToOpen();
+ EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 2U);
+ EXPECT_EQ(chrome::GetTotalBrowserCount(), 2U);
+ EXPECT_NE(browser()->profile(), new_browser->profile());
+
+ // Confirm sync in the new browser window.
+ SignInTestObserver new_browser_observer(identity_manager(new_browser),
+ account_reconcilor(new_browser));
+ EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog(
+ new_browser, kDialogTimeout));
+ new_browser_observer.WaitForAccountChanges(
+ 1, PrimarySyncAccountWait::kWaitForAdded);
+
+ // Check accounts in cookies in the new profile.
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar =
+ identity_manager(new_browser)->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh);
+ ASSERT_EQ(1u, accounts_in_cookie_jar.signed_in_accounts.size());
+ const gaia::ListedAccount& account =
+ accounts_in_cookie_jar.signed_in_accounts[0];
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, account.email));
+
+ // Check the primary account in the new profile is set and syncing.
+ const CoreAccountInfo& primary_account =
+ identity_manager(new_browser)
+ ->GetPrimaryAccountInfo(signin::ConsentLevel::kSync);
+ EXPECT_FALSE(primary_account.IsEmpty());
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, primary_account.email));
+ EXPECT_TRUE(identity_manager(new_browser)
+ ->HasAccountWithRefreshToken(primary_account.account_id));
+ EXPECT_TRUE(sync_service(new_browser)->IsSyncFeatureEnabled());
+
+ // Check that the second account was removed from the original profile.
+ original_browser_observer.WaitForAccountChanges(
+ 0, PrimarySyncAccountWait::kWaitForCleared);
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar_2 =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar_2.accounts_are_fresh);
+ ASSERT_TRUE(accounts_in_cookie_jar_2.signed_in_accounts.empty());
+ EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty());
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+}
+
+// This test can pass. Marked as manual because it TIMED_OUT on Win7.
+// See crbug.com/1025335.
+// Enables and disables sync to account 1. Enables sync to account 2 and clicks
+// on "This was me" in the email confirmation dialog. Checks that Sync to
+// account 2 is enabled in the current profile.
+IN_PROC_BROWSER_TEST_F(LiveSignInTest,
+ MANUAL_SyncSecondAccount_InExistingProfile) {
+ // Enable and disable sync for the first account.
+ TestAccount test_account_1;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1));
+ TurnOnSync(test_account_1, 0);
+ TurnOffSync();
+
+ // Start enable sync for the second account.
+ TestAccount test_account_2;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2));
+ SignInFromSettings(test_account_2, 0);
+
+ // Check there is only one profile.
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U);
+ EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U);
+
+ // Click "This was me" on the email confirmation dialog, confirm sync and wait
+ // for a primary account to be set.
+ SignInTestObserver observer(identity_manager(), account_reconcilor());
+ EXPECT_TRUE(login_ui_test_utils::CompleteSigninEmailConfirmationDialog(
+ browser(), kDialogTimeout, SigninEmailConfirmationDialog::START_SYNC));
+ EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog(
+ browser(), kDialogTimeout));
+ observer.WaitForAccountChanges(1, PrimarySyncAccountWait::kWaitForAdded);
+
+ // Check no profile was created.
+ EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U);
+ EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U);
+
+ // Check accounts in cookies.
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh);
+ ASSERT_EQ(1u, accounts_in_cookie_jar.signed_in_accounts.size());
+ const gaia::ListedAccount& account =
+ accounts_in_cookie_jar.signed_in_accounts[0];
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, account.email));
+
+ // Check the primary account is set and syncing.
+ const CoreAccountInfo& primary_account =
+ identity_manager()->GetPrimaryAccountInfo(signin::ConsentLevel::kSync);
+ EXPECT_FALSE(primary_account.IsEmpty());
+ EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, primary_account.email));
+ EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
+ primary_account.account_id));
+ EXPECT_TRUE(sync_service()->IsSyncFeatureEnabled());
+}
+
+// This test can pass. Marked as manual because it TIMED_OUT on Win7.
+// See crbug.com/1025335.
+// Enables and disables sync to account 1. Enables sync to account 2 and clicks
+// on "Cancel" in the email confirmation dialog. Checks that the signin flow is
+// canceled and no accounts are added to Chrome.
+IN_PROC_BROWSER_TEST_F(LiveSignInTest,
+ MANUAL_SyncSecondAccount_CancelOnEmailConfirmation) {
+ // Enable and disable sync for the first account.
+ TestAccount test_account_1;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1));
+ TurnOnSync(test_account_1, 0);
+ TurnOffSync();
+
+ // Start enable sync for the second account.
+ TestAccount test_account_2;
+ CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2));
+ SignInFromSettings(test_account_2, 0);
+
+ // Check there is only one profile.
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U);
+ EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U);
+
+ // Click "Cancel" on the email confirmation dialog and wait for an account to
+ // removed from Chrome.
+ SignInTestObserver observer(identity_manager(), account_reconcilor());
+ EXPECT_TRUE(login_ui_test_utils::CompleteSigninEmailConfirmationDialog(
+ browser(), kDialogTimeout, SigninEmailConfirmationDialog::CLOSE));
+ observer.WaitForAccountChanges(0, PrimarySyncAccountWait::kWaitForCleared);
+
+ // Check no profile was created.
+ EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U);
+ EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U);
+
+ // Check Chrome has no accounts.
+ const AccountsInCookieJarInfo& accounts_in_cookie_jar =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh);
+ EXPECT_TRUE(accounts_in_cookie_jar.signed_in_accounts.empty());
+ EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty());
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+}
+
+IN_PROC_BROWSER_TEST_F(LiveSignInTest,
+ MANUAL_AccountCapabilities_FetchedOnSignIn) {
+ EnableAccountCapabilitiesFetches(identity_manager());
+
+ // Test primary adult account.
+ {
+ AccountCapabilitiesObserver capabilities_observer(identity_manager());
+
+ TestAccount ta;
+ ASSERT_TRUE(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", ta));
+ SignInFromSettings(ta, 0);
+
+ CoreAccountInfo core_account_info =
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin);
+ ASSERT_TRUE(gaia::AreEmailsSame(core_account_info.email, ta.user));
+
+ capabilities_observer.WaitForAllCapabilitiesToBeKnown(
+ core_account_info.account_id);
+ AccountInfo account_info =
+ identity_manager()->FindExtendedAccountInfoByAccountId(
+ core_account_info.account_id);
+ EXPECT_EQ(account_info.capabilities.can_offer_extended_chrome_sync_promos(),
+ Tribool::kTrue);
+ }
+
+ // Test secondary minor account.
+ {
+ AccountCapabilitiesObserver capabilities_observer(identity_manager());
+
+ TestAccount ta;
+ ASSERT_TRUE(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_MINOR", ta));
+ SignInFromWeb(ta, /*previously_signed_in_accounts=*/1);
+
+ CoreAccountInfo core_account_info =
+ identity_manager()->FindExtendedAccountInfoByEmailAddress(ta.user);
+ ASSERT_FALSE(core_account_info.IsEmpty());
+
+ capabilities_observer.WaitForAllCapabilitiesToBeKnown(
+ core_account_info.account_id);
+ AccountInfo account_info =
+ identity_manager()->FindExtendedAccountInfoByAccountId(
+ core_account_info.account_id);
+ EXPECT_EQ(account_info.capabilities.can_offer_extended_chrome_sync_promos(),
+ Tribool::kFalse);
+ }
+}
+
+} // namespace test
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/e2e_tests/live_test.cc b/chromium/chrome/browser/signin/e2e_tests/live_test.cc
new file mode 100644
index 00000000000..e8d22189e73
--- /dev/null
+++ b/chromium/chrome/browser/signin/e2e_tests/live_test.cc
@@ -0,0 +1,61 @@
+// Copyright 2019 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.
+#include "chrome/browser/signin/e2e_tests/live_test.h"
+
+#include "base/files/file_util.h"
+#include "base/path_service.h"
+#include "net/dns/mock_host_resolver.h"
+
+base::FilePath::StringPieceType kTestAccountFilePath = FILE_PATH_LITERAL(
+ "chrome/browser/internal/resources/signin/test_accounts.json");
+
+const char* kRunLiveTestFlag = "run-live-tests";
+
+namespace signin {
+namespace test {
+
+void LiveTest::SetUpInProcessBrowserTestFixture() {
+ // Whitelists a bunch of hosts.
+ host_resolver()->AllowDirectLookup("*.google.com");
+ host_resolver()->AllowDirectLookup("*.geotrust.com");
+ host_resolver()->AllowDirectLookup("*.gstatic.com");
+ host_resolver()->AllowDirectLookup("*.googleapis.com");
+ // Allows country-specific TLDs.
+ host_resolver()->AllowDirectLookup("accounts.google.*");
+
+ InProcessBrowserTest::SetUpInProcessBrowserTestFixture();
+}
+
+void LiveTest::SetUp() {
+ // Only run live tests when specified.
+ auto* cmd_line = base::CommandLine::ForCurrentProcess();
+ if (!cmd_line->HasSwitch(kRunLiveTestFlag)) {
+ LOG(INFO) << "This test should get skipped.";
+ skip_test_ = true;
+ GTEST_SKIP();
+ }
+ base::FilePath root_path;
+ base::PathService::Get(base::BasePathKey::DIR_SOURCE_ROOT, &root_path);
+ base::FilePath config_path =
+ base::MakeAbsoluteFilePath(root_path.Append(kTestAccountFilePath));
+ test_accounts_.Init(config_path);
+ InProcessBrowserTest::SetUp();
+}
+
+void LiveTest::TearDown() {
+ // This test was skipped, no need to tear down.
+ if (skip_test_)
+ return;
+ InProcessBrowserTest::TearDown();
+}
+
+void LiveTest::PostRunTestOnMainThread() {
+ // This test was skipped. Running PostRunTestOnMainThread can cause
+ // TIMED_OUT on Win7.
+ if (skip_test_)
+ return;
+ InProcessBrowserTest::PostRunTestOnMainThread();
+}
+} // namespace test
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/e2e_tests/live_test.h b/chromium/chrome/browser/signin/e2e_tests/live_test.h
new file mode 100644
index 00000000000..6de9f8f4ded
--- /dev/null
+++ b/chromium/chrome/browser/signin/e2e_tests/live_test.h
@@ -0,0 +1,33 @@
+// Copyright 2019 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_SIGNIN_E2E_TESTS_LIVE_TEST_H_
+#define CHROME_BROWSER_SIGNIN_E2E_TESTS_LIVE_TEST_H_
+
+#include "chrome/browser/signin/e2e_tests/test_accounts_util.h"
+#include "chrome/test/base/in_process_browser_test.h"
+
+namespace signin {
+namespace test {
+
+class LiveTest : public InProcessBrowserTest {
+ protected:
+ void SetUpInProcessBrowserTestFixture() override;
+ void SetUp() override;
+ void TearDown() override;
+ void PostRunTestOnMainThread() override;
+
+ const TestAccountsUtil* GetTestAccountsUtil() const {
+ return &test_accounts_;
+ }
+
+ private:
+ TestAccountsUtil test_accounts_;
+ bool skip_test_ = false;
+};
+
+} // namespace test
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_E2E_TESTS_LIVE_TEST_H_
diff --git a/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.cc b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.cc
new file mode 100644
index 00000000000..c533ea0fb3c
--- /dev/null
+++ b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.cc
@@ -0,0 +1,75 @@
+// Copyright 2019 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.
+
+#include "chrome/browser/signin/e2e_tests/test_accounts_util.h"
+
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/json/json_file_value_serializer.h"
+#include "base/json/json_reader.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+
+using base::Value;
+
+namespace signin {
+namespace test {
+
+#if defined(OS_WIN)
+std::string kPlatform = "win";
+#elif defined(OS_MAC)
+std::string kPlatform = "mac";
+#elif BUILDFLAG(IS_CHROMEOS_ASH)
+std::string kPlatform = "chromeos";
+#elif defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS)
+std::string kPlatform = "linux";
+#elif defined(OS_ANDROID)
+std::string kPlatform = "android";
+#else
+std::string kPlatform = "all_platform";
+#endif
+
+TestAccountsUtil::TestAccountsUtil() = default;
+TestAccountsUtil::~TestAccountsUtil() = default;
+
+bool TestAccountsUtil::Init(const base::FilePath& config_path) {
+ int error_code = 0;
+ std::string error_str;
+ JSONFileValueDeserializer deserializer(config_path);
+ std::unique_ptr<Value> content_json =
+ deserializer.Deserialize(&error_code, &error_str);
+ CHECK(error_code == 0) << "Error reading json file. Error code: "
+ << error_code << " " << error_str;
+ CHECK(content_json);
+
+ // Only store platform specific users. If an account does not have
+ // platform specific user, try to use all_platform user.
+ for (auto account : content_json->DictItems()) {
+ const Value* platform_account = account.second.FindDictKey(kPlatform);
+ if (platform_account == nullptr) {
+ platform_account = account.second.FindDictKey("all_platform");
+ if (platform_account == nullptr) {
+ continue;
+ }
+ }
+ TestAccount ta(*(platform_account->FindStringKey("user")),
+ *(platform_account->FindStringKey("password")));
+ all_accounts_.insert(
+ std::pair<std::string, TestAccount>(account.first, ta));
+ }
+ return true;
+}
+
+bool TestAccountsUtil::GetAccount(const std::string& name,
+ TestAccount& out_account) const {
+ auto it = all_accounts_.find(name);
+ if (it == all_accounts_.end()) {
+ return false;
+ }
+ out_account = it->second;
+ return true;
+}
+
+} // namespace test
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.h b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.h
new file mode 100644
index 00000000000..4f73dd76d32
--- /dev/null
+++ b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.h
@@ -0,0 +1,46 @@
+// Copyright 2018 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_SIGNIN_E2E_TESTS_TEST_ACCOUNTS_UTIL_H_
+#define CHROME_BROWSER_SIGNIN_E2E_TESTS_TEST_ACCOUNTS_UTIL_H_
+
+#include <map>
+#include <string>
+
+namespace base {
+class FilePath;
+}
+
+namespace signin {
+namespace test {
+
+struct TestAccount {
+ std::string user;
+ std::string password;
+ TestAccount() = default;
+ TestAccount(const std::string& user, const std::string& password) {
+ this->user = user;
+ this->password = password;
+ }
+};
+
+class TestAccountsUtil {
+ public:
+ TestAccountsUtil();
+
+ TestAccountsUtil(const TestAccountsUtil&) = delete;
+ TestAccountsUtil& operator=(const TestAccountsUtil&) = delete;
+
+ virtual ~TestAccountsUtil();
+ bool Init(const base::FilePath& config_path);
+ bool GetAccount(const std::string& name, TestAccount& out_account) const;
+
+ private:
+ std::map<std::string, TestAccount> all_accounts_;
+};
+
+} // namespace test
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_E2E_TESTS_TEST_ACCOUNTS_UTIL_H_
diff --git a/chromium/chrome/browser/signin/e2e_tests/test_accounts_util_unittest.cc b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util_unittest.cc
new file mode 100644
index 00000000000..7b63dd6128a
--- /dev/null
+++ b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util_unittest.cc
@@ -0,0 +1,100 @@
+// Copyright 2019 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.
+
+#include "chrome/browser/signin/e2e_tests/test_accounts_util.h"
+#include "base/files/file_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using base::FilePath;
+
+namespace signin {
+namespace test {
+
+class TestAccountsUtilTest : public testing::Test {};
+
+FilePath WriteContentToTemporaryFile(const char* contents,
+ unsigned int length) {
+ FilePath tmp_file;
+ CHECK(base::CreateTemporaryFile(&tmp_file));
+ unsigned int bytes_written = base::WriteFile(tmp_file, contents, length);
+ CHECK_EQ(bytes_written, length);
+ return tmp_file;
+}
+
+TEST(TestAccountsUtilTest, ParsingJson) {
+ const char contents[] =
+ "{ \n"
+ " \"TEST_ACCOUNT_1\": {\n"
+ " \"win\": {\n"
+ " \"user\": \"user1\",\n"
+ " \"password\": \"pwd1\"\n"
+ " }\n"
+ " }\n"
+ "}";
+ FilePath tmp_file =
+ WriteContentToTemporaryFile(contents, sizeof(contents) - 1);
+ TestAccountsUtil util;
+ util.Init(tmp_file);
+}
+
+TEST(TestAccountsUtilTest, GetAccountForPlatformSpecific) {
+ const char contents[] =
+ "{ \n"
+ " \"TEST_ACCOUNT_1\": {\n"
+ " \"win\": {\n"
+ " \"user\": \"user1\",\n"
+ " \"password\": \"pwd1\"\n"
+ " },\n"
+ " \"mac\": {\n"
+ " \"user\": \"user1\",\n"
+ " \"password\": \"pwd1\"\n"
+ " },\n"
+ " \"linux\": {\n"
+ " \"user\": \"user1\",\n"
+ " \"password\": \"pwd1\"\n"
+ " },\n"
+ " \"chromeos\": {\n"
+ " \"user\": \"user1\",\n"
+ " \"password\": \"pwd1\"\n"
+ " },\n"
+ " \"android\": {\n"
+ " \"user\": \"user1\",\n"
+ " \"password\": \"pwd1\"\n"
+ " }\n"
+ " }\n"
+ "}";
+ FilePath tmp_file =
+ WriteContentToTemporaryFile(contents, sizeof(contents) - 1);
+ TestAccountsUtil util;
+ util.Init(tmp_file);
+ TestAccount ta;
+ bool ret = util.GetAccount("TEST_ACCOUNT_1", ta);
+ ASSERT_TRUE(ret);
+ ASSERT_EQ(ta.user, "user1");
+ ASSERT_EQ(ta.password, "pwd1");
+}
+
+TEST(TestAccountsUtilTest, GetAccountForAllPlatform) {
+ const char contents[] =
+ "{ \n"
+ " \"TEST_ACCOUNT_1\": {\n"
+ " \"all_platform\": {\n"
+ " \"user\": \"user_allplatform\",\n"
+ " \"password\": \"pwd_allplatform\"\n"
+ " }\n"
+ " }\n"
+ "}";
+ FilePath tmp_file =
+ WriteContentToTemporaryFile(contents, sizeof(contents) - 1);
+ TestAccountsUtil util;
+ util.Init(tmp_file);
+ TestAccount ta;
+ bool ret = util.GetAccount("TEST_ACCOUNT_1", ta);
+ ASSERT_TRUE(ret);
+ ASSERT_EQ(ta.user, "user_allplatform");
+ ASSERT_EQ(ta.password, "pwd_allplatform");
+}
+
+} // namespace test
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/force_signin_verifier.cc b/chromium/chrome/browser/signin/force_signin_verifier.cc
new file mode 100644
index 00000000000..951efcd003c
--- /dev/null
+++ b/chromium/chrome/browser/signin/force_signin_verifier.cc
@@ -0,0 +1,195 @@
+// Copyright 2017 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.
+
+#include <string>
+
+#include "chrome/browser/signin/force_signin_verifier.h"
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/files/file_path.h"
+#include "base/metrics/histogram_macros.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/browser/ui/profile_picker.h"
+#include "chrome/browser/ui/ui_features.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "components/signin/public/identity_manager/access_token_info.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/primary_account_access_token_fetcher.h"
+#include "components/signin/public/identity_manager/primary_account_mutator.h"
+#include "components/signin/public/identity_manager/scope_set.h"
+#include "content/public/browser/network_service_instance.h"
+#include "google_apis/gaia/gaia_constants.h"
+
+namespace {
+const net::BackoffEntry::Policy kForceSigninVerifierBackoffPolicy = {
+ 0, // Number of initial errors to ignore before applying
+ // exponential back-off rules.
+ 2000, // Initial delay in ms.
+ 2, // Factor by which the waiting time will be multiplied.
+ 0.2, // Fuzzing percentage.
+ 4 * 60 * 1000, // Maximum amount of time to delay th request in ms.
+ -1, // Never discard the entry.
+ false // Do not always use initial delay.
+};
+
+} // namespace
+
+ForceSigninVerifier::ForceSigninVerifier(
+ Profile* profile,
+ signin::IdentityManager* identity_manager)
+ : has_token_verified_(false),
+ backoff_entry_(&kForceSigninVerifierBackoffPolicy),
+ creation_time_(base::TimeTicks::Now()),
+ profile_(profile),
+ identity_manager_(identity_manager) {
+ content::GetNetworkConnectionTracker()->AddNetworkConnectionObserver(this);
+ // Most of time (~94%), sign-in token can be verified with server.
+ SendRequest();
+}
+
+ForceSigninVerifier::~ForceSigninVerifier() {
+ Cancel();
+}
+
+void ForceSigninVerifier::OnAccessTokenFetchComplete(
+ GoogleServiceAuthError error,
+ signin::AccessTokenInfo token_info) {
+ if (error.state() != GoogleServiceAuthError::NONE) {
+ if (error.IsPersistentError()) {
+ // Based on the obsolete UMA Signin.ForceSigninVerificationTime.Failure,
+ // about 7% verifications are failed. Most of them are finished within
+ // 113ms but some of them (<3%) could take longer than 3 minutes.
+ has_token_verified_ = true;
+ CloseAllBrowserWindows();
+ content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver(
+ this);
+ Cancel();
+ } else {
+ backoff_entry_.InformOfRequest(false);
+ backoff_request_timer_.Start(
+ FROM_HERE, backoff_entry_.GetTimeUntilRelease(),
+ base::BindOnce(&ForceSigninVerifier::SendRequest,
+ weak_factory_.GetWeakPtr()));
+ access_token_fetcher_.reset();
+ }
+ return;
+ }
+
+ // Based on the obsolete UMA Signin.ForceSigninVerificationTime.Success, about
+ // 93% verifications are succeeded. Most of them are finished ~1 second but
+ // some of them (<3%) could take longer than 3 minutes.
+ has_token_verified_ = true;
+ content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver(this);
+ Cancel();
+}
+
+void ForceSigninVerifier::OnConnectionChanged(
+ network::mojom::ConnectionType type) {
+ // Try again immediately once the network is back and cancel any pending
+ // request.
+ backoff_entry_.Reset();
+ if (backoff_request_timer_.IsRunning())
+ backoff_request_timer_.Stop();
+
+ SendRequestIfNetworkAvailable(type);
+}
+
+void ForceSigninVerifier::Cancel() {
+ backoff_entry_.Reset();
+ backoff_request_timer_.Stop();
+ access_token_fetcher_.reset();
+ content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver(this);
+}
+
+bool ForceSigninVerifier::HasTokenBeenVerified() {
+ return has_token_verified_;
+}
+
+void ForceSigninVerifier::SendRequest() {
+ auto type = network::mojom::ConnectionType::CONNECTION_NONE;
+ if (content::GetNetworkConnectionTracker()->GetConnectionType(
+ &type,
+ base::BindOnce(&ForceSigninVerifier::SendRequestIfNetworkAvailable,
+ weak_factory_.GetWeakPtr()))) {
+ SendRequestIfNetworkAvailable(type);
+ }
+}
+
+void ForceSigninVerifier::SendRequestIfNetworkAvailable(
+ network::mojom::ConnectionType network_type) {
+ if (network_type == network::mojom::ConnectionType::CONNECTION_NONE ||
+ !ShouldSendRequest()) {
+ return;
+ }
+
+ signin::ScopeSet oauth2_scopes;
+ oauth2_scopes.insert(GaiaConstants::kChromeSyncOAuth2Scope);
+ access_token_fetcher_ =
+ std::make_unique<signin::PrimaryAccountAccessTokenFetcher>(
+ "force_signin_verifier", identity_manager_, oauth2_scopes,
+ base::BindOnce(&ForceSigninVerifier::OnAccessTokenFetchComplete,
+ weak_factory_.GetWeakPtr()),
+ signin::PrimaryAccountAccessTokenFetcher::Mode::kImmediate);
+}
+
+bool ForceSigninVerifier::ShouldSendRequest() {
+ return !has_token_verified_ && access_token_fetcher_.get() == nullptr &&
+ identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync);
+}
+
+void ForceSigninVerifier::CloseAllBrowserWindows() {
+ if (base::FeatureList::IsEnabled(features::kForceSignInReauth)) {
+ // Do not sign the user out to allow them to reauthenticate from the profile
+ // picker.
+ BrowserList::CloseAllBrowsersWithProfile(
+ profile_,
+ base::BindRepeating(&ForceSigninVerifier::OnCloseBrowsersSuccess,
+ weak_factory_.GetWeakPtr()),
+ base::DoNothing(),
+ /*skip_beforeunload=*/true);
+ } else {
+ // Do not close window if there is ongoing reauth. If it fails later, the
+ // signin process should take care of the signout.
+ auto* primary_account_mutator =
+ identity_manager_->GetPrimaryAccountMutator();
+ if (!primary_account_mutator)
+ return;
+ primary_account_mutator->ClearPrimaryAccount(
+ signin_metrics::AUTHENTICATION_FAILED_WITH_FORCE_SIGNIN,
+ signin_metrics::SignoutDelete::kIgnoreMetric);
+ }
+}
+
+void ForceSigninVerifier::OnCloseBrowsersSuccess(
+ const base::FilePath& profile_path) {
+ Cancel();
+
+ ProfileAttributesEntry* entry =
+ g_browser_process->profile_manager()
+ ->GetProfileAttributesStorage()
+ .GetProfileAttributesWithPath(profile_path);
+ if (!entry)
+ return;
+ entry->LockForceSigninProfile(true);
+ ProfilePicker::Show(ProfilePicker::EntryPoint::kProfileLocked);
+}
+
+signin::PrimaryAccountAccessTokenFetcher*
+ForceSigninVerifier::GetAccessTokenFetcherForTesting() {
+ return access_token_fetcher_.get();
+}
+
+net::BackoffEntry* ForceSigninVerifier::GetBackoffEntryForTesting() {
+ return &backoff_entry_;
+}
+
+base::OneShotTimer* ForceSigninVerifier::GetOneShotTimerForTesting() {
+ return &backoff_request_timer_;
+}
diff --git a/chromium/chrome/browser/signin/force_signin_verifier.h b/chromium/chrome/browser/signin/force_signin_verifier.h
new file mode 100644
index 00000000000..7260aa6f3fe
--- /dev/null
+++ b/chromium/chrome/browser/signin/force_signin_verifier.h
@@ -0,0 +1,95 @@
+// Copyright 2017 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_SIGNIN_FORCE_SIGNIN_VERIFIER_H_
+#define CHROME_BROWSER_SIGNIN_FORCE_SIGNIN_VERIFIER_H_
+
+#include <memory>
+
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/time/time.h"
+#include "base/timer/timer.h"
+#include "google_apis/gaia/google_service_auth_error.h"
+#include "net/base/backoff_entry.h"
+#include "services/network/public/cpp/network_connection_tracker.h"
+
+class Profile;
+
+namespace base {
+class FilePath;
+}
+
+namespace signin {
+class IdentityManager;
+class PrimaryAccountAccessTokenFetcher;
+struct AccessTokenInfo;
+} // namespace signin
+
+// ForceSigninVerifier will verify profile's auth token when profile is loaded
+// into memory by the first time via gaia server. It will retry on any transient
+// error.
+class ForceSigninVerifier
+ : public network::NetworkConnectionTracker::NetworkConnectionObserver {
+ public:
+ explicit ForceSigninVerifier(Profile* profile,
+ signin::IdentityManager* identity_manager);
+
+ ForceSigninVerifier(const ForceSigninVerifier&) = delete;
+ ForceSigninVerifier& operator=(const ForceSigninVerifier&) = delete;
+
+ ~ForceSigninVerifier() override;
+
+ void OnAccessTokenFetchComplete(GoogleServiceAuthError error,
+ signin::AccessTokenInfo token_info);
+
+ // override network::NetworkConnectionTracker::NetworkConnectionObserver
+ void OnConnectionChanged(network::mojom::ConnectionType type) override;
+
+ // Cancel any pending or ongoing verification.
+ void Cancel();
+
+ // Return the value of |has_token_verified_|.
+ bool HasTokenBeenVerified();
+
+ protected:
+ // Send the token verification request. The request will be sent only if
+ // - The token has never been verified before.
+ // - There is no on going verification.
+ // - There is network connection.
+ // - The profile has signed in.
+ void SendRequest();
+
+ // Send the request if |network_type| is not CONNECTION_NONE and
+ // ShouldSendRequest returns true.
+ void SendRequestIfNetworkAvailable(
+ network::mojom::ConnectionType network_type);
+
+ bool ShouldSendRequest();
+
+ virtual void CloseAllBrowserWindows();
+ void OnCloseBrowsersSuccess(const base::FilePath& profile_path);
+
+ signin::PrimaryAccountAccessTokenFetcher* GetAccessTokenFetcherForTesting();
+ net::BackoffEntry* GetBackoffEntryForTesting();
+ base::OneShotTimer* GetOneShotTimerForTesting();
+
+ private:
+ std::unique_ptr<signin::PrimaryAccountAccessTokenFetcher>
+ access_token_fetcher_;
+
+ // Indicates whether the verification is finished successfully or with a
+ // persistent error.
+ bool has_token_verified_ = false;
+ net::BackoffEntry backoff_entry_;
+ base::OneShotTimer backoff_request_timer_;
+ base::TimeTicks creation_time_;
+
+ raw_ptr<Profile> profile_ = nullptr;
+ raw_ptr<signin::IdentityManager> identity_manager_ = nullptr;
+
+ base::WeakPtrFactory<ForceSigninVerifier> weak_factory_{this};
+};
+
+#endif // CHROME_BROWSER_SIGNIN_FORCE_SIGNIN_VERIFIER_H_
diff --git a/chromium/chrome/browser/signin/force_signin_verifier_unittest.cc b/chromium/chrome/browser/signin/force_signin_verifier_unittest.cc
new file mode 100644
index 00000000000..dcd382d6b38
--- /dev/null
+++ b/chromium/chrome/browser/signin/force_signin_verifier_unittest.cc
@@ -0,0 +1,416 @@
+// Copyright 2017 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.
+
+#include "chrome/browser/signin/force_signin_verifier.h"
+
+#include "base/run_loop.h"
+#include "base/test/task_environment.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "chrome/browser/profiles/profile.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "content/public/browser/network_service_instance.h"
+#include "services/network/test/test_network_connection_tracker.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+class ForceSigninVerifierWithAccessToInternalsForTesting
+ : public ForceSigninVerifier {
+ public:
+ explicit ForceSigninVerifierWithAccessToInternalsForTesting(
+ signin::IdentityManager* identity_manager)
+ : ForceSigninVerifier(nullptr, identity_manager) {}
+
+ bool IsDelayTaskPosted() { return GetOneShotTimerForTesting()->IsRunning(); }
+
+ int FailureCount() { return GetBackoffEntryForTesting()->failure_count(); }
+
+ signin::PrimaryAccountAccessTokenFetcher* access_token_fetcher() {
+ return GetAccessTokenFetcherForTesting();
+ }
+
+ MOCK_METHOD0(CloseAllBrowserWindows, void(void));
+};
+
+// A NetworkConnectionObserver that invokes a base::RepeatingClosure when
+// NetworkConnectionObserver::OnConnectionChanged() is invoked.
+class NetworkConnectionObserverHelper
+ : public network::NetworkConnectionTracker::NetworkConnectionObserver {
+ public:
+ explicit NetworkConnectionObserverHelper(base::RepeatingClosure closure)
+ : closure_(std::move(closure)) {
+ DCHECK(!closure_.is_null());
+ content::GetNetworkConnectionTracker()->AddNetworkConnectionObserver(this);
+ }
+
+ NetworkConnectionObserverHelper(const NetworkConnectionObserverHelper&) =
+ delete;
+ NetworkConnectionObserverHelper& operator=(
+ const NetworkConnectionObserverHelper&) = delete;
+
+ ~NetworkConnectionObserverHelper() override {
+ content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver(
+ this);
+ }
+
+ void OnConnectionChanged(network::mojom::ConnectionType type) override {
+ closure_.Run();
+ }
+
+ private:
+ base::RepeatingClosure closure_;
+};
+
+// Used to select which type of network type NetworkConnectionTracker should
+// be configured to.
+enum class NetworkConnectionType {
+ Undecided,
+ ConnectionNone,
+ ConnectionWifi,
+ Connection4G,
+};
+
+// Used to select which type of response NetworkConnectionTracker should give.
+enum class NetworkResponseType {
+ Undecided,
+ Synchronous,
+ Asynchronous,
+};
+
+// Forces the network connection type to change to |connection_type| and wait
+// till the notification has been propagated to the observers. Also change the
+// response type to be synchronous/asynchronous based on |response_type|.
+void ConfigureNetworkConnectionTracker(NetworkConnectionType connection_type,
+ NetworkResponseType response_type) {
+ network::TestNetworkConnectionTracker* tracker =
+ network::TestNetworkConnectionTracker::GetInstance();
+
+ switch (response_type) {
+ case NetworkResponseType::Undecided:
+ // nothing to do
+ break;
+
+ case NetworkResponseType::Synchronous:
+ tracker->SetRespondSynchronously(true);
+ break;
+
+ case NetworkResponseType::Asynchronous:
+ tracker->SetRespondSynchronously(false);
+ break;
+ }
+
+ if (connection_type != NetworkConnectionType::Undecided) {
+ network::mojom::ConnectionType mojom_connection_type =
+ network::mojom::ConnectionType::CONNECTION_UNKNOWN;
+
+ switch (connection_type) {
+ case NetworkConnectionType::Undecided:
+ NOTREACHED();
+ break;
+
+ case NetworkConnectionType::ConnectionNone:
+ mojom_connection_type = network::mojom::ConnectionType::CONNECTION_NONE;
+ break;
+
+ case NetworkConnectionType::ConnectionWifi:
+ mojom_connection_type = network::mojom::ConnectionType::CONNECTION_WIFI;
+ break;
+
+ case NetworkConnectionType::Connection4G:
+ mojom_connection_type = network::mojom::ConnectionType::CONNECTION_4G;
+ break;
+ }
+
+ DCHECK_NE(mojom_connection_type,
+ network::mojom::ConnectionType::CONNECTION_UNKNOWN);
+
+ base::RunLoop wait_for_network_type_change;
+ NetworkConnectionObserverHelper scoped_observer(
+ wait_for_network_type_change.QuitWhenIdleClosure());
+
+ tracker->SetConnectionType(mojom_connection_type);
+
+ wait_for_network_type_change.Run();
+ }
+}
+
+// Forces the current sequence's task runner to spin. This is used because the
+// ForceSigninVerifier ends up posting task to the sequence's task runner when
+// MetworkConnectionTracker is returning results asynchronously.
+void SpinCurrentSequenceTaskRunner() {
+ base::RunLoop run_loop;
+ base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE,
+ run_loop.QuitClosure());
+ run_loop.Run();
+}
+
+} // namespace
+
+TEST(ForceSigninVerifierTest, OnGetTokenSuccess) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ ASSERT_NE(nullptr, verifier.access_token_fetcher());
+ ASSERT_FALSE(verifier.HasTokenBeenVerified());
+ ASSERT_FALSE(verifier.IsDelayTaskPosted());
+ EXPECT_CALL(verifier, CloseAllBrowserWindows()).Times(0);
+
+ identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
+ account_info.account_id, /*token=*/"", base::Time());
+
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+ ASSERT_TRUE(verifier.HasTokenBeenVerified());
+ ASSERT_FALSE(verifier.IsDelayTaskPosted());
+ ASSERT_EQ(0, verifier.FailureCount());
+}
+
+TEST(ForceSigninVerifierTest, OnGetTokenPersistentFailure) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ ASSERT_NE(nullptr, verifier.access_token_fetcher());
+ ASSERT_FALSE(verifier.HasTokenBeenVerified());
+ ASSERT_FALSE(verifier.IsDelayTaskPosted());
+ EXPECT_CALL(verifier, CloseAllBrowserWindows()).Times(1);
+
+ identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithError(
+ GoogleServiceAuthError(
+ GoogleServiceAuthError::State::INVALID_GAIA_CREDENTIALS));
+
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+ ASSERT_TRUE(verifier.HasTokenBeenVerified());
+ ASSERT_FALSE(verifier.IsDelayTaskPosted());
+ ASSERT_EQ(0, verifier.FailureCount());
+}
+
+TEST(ForceSigninVerifierTest, OnGetTokenTransientFailure) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ ASSERT_NE(nullptr, verifier.access_token_fetcher());
+ ASSERT_FALSE(verifier.HasTokenBeenVerified());
+ ASSERT_FALSE(verifier.IsDelayTaskPosted());
+ EXPECT_CALL(verifier, CloseAllBrowserWindows()).Times(0);
+
+ identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithError(
+ GoogleServiceAuthError(GoogleServiceAuthError::State::CONNECTION_FAILED));
+
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+ ASSERT_FALSE(verifier.HasTokenBeenVerified());
+ ASSERT_TRUE(verifier.IsDelayTaskPosted());
+ ASSERT_EQ(1, verifier.FailureCount());
+}
+
+TEST(ForceSigninVerifierTest, OnLostConnection) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithError(
+ GoogleServiceAuthError(GoogleServiceAuthError::State::CONNECTION_FAILED));
+
+ ASSERT_EQ(1, verifier.FailureCount());
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+ ASSERT_TRUE(verifier.IsDelayTaskPosted());
+
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionNone,
+ NetworkResponseType::Undecided);
+
+ ASSERT_EQ(0, verifier.FailureCount());
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+ ASSERT_FALSE(verifier.IsDelayTaskPosted());
+}
+
+TEST(ForceSigninVerifierTest, OnReconnected) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithError(
+ GoogleServiceAuthError(GoogleServiceAuthError::State::CONNECTION_FAILED));
+
+ ASSERT_EQ(1, verifier.FailureCount());
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+ ASSERT_TRUE(verifier.IsDelayTaskPosted());
+
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionWifi,
+ NetworkResponseType::Undecided);
+
+ ASSERT_EQ(0, verifier.FailureCount());
+ ASSERT_NE(nullptr, verifier.access_token_fetcher());
+ ASSERT_FALSE(verifier.IsDelayTaskPosted());
+}
+
+TEST(ForceSigninVerifierTest, GetNetworkStatusAsync) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::Undecided,
+ NetworkResponseType::Asynchronous);
+
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ // There is no network type at first.
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+
+ // Waiting for the network type returns.
+ SpinCurrentSequenceTaskRunner();
+
+ // Get the type and send the request.
+ ASSERT_NE(nullptr, verifier.access_token_fetcher());
+}
+
+TEST(ForceSigninVerifierTest, LaunchVerifierWithoutNetwork) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionNone,
+ NetworkResponseType::Asynchronous);
+
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ // There is no network type.
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+
+ // Waiting for the network type returns.
+ SpinCurrentSequenceTaskRunner();
+
+ // Get the type, there is no network connection, don't send the request.
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+
+ // Network is resumed.
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionWifi,
+ NetworkResponseType::Undecided);
+
+ // Send the request.
+ ASSERT_NE(nullptr, verifier.access_token_fetcher());
+}
+
+TEST(ForceSigninVerifierTest, ChangeNetworkFromWIFITo4GWithOnGoingRequest) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionWifi,
+ NetworkResponseType::Asynchronous);
+
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ EXPECT_EQ(nullptr, verifier.access_token_fetcher());
+
+ // Waiting for the network type returns.
+ SpinCurrentSequenceTaskRunner();
+
+ // The network type if wifi, send the request.
+ auto* first_request = verifier.access_token_fetcher();
+ EXPECT_NE(nullptr, first_request);
+
+ // Network is changed to 4G.
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::Connection4G,
+ NetworkResponseType::Undecided);
+
+ // There is still one on-going request.
+ EXPECT_EQ(first_request, verifier.access_token_fetcher());
+ identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
+ account_info.account_id, /*token=*/"", base::Time());
+}
+
+TEST(ForceSigninVerifierTest, ChangeNetworkFromWIFITo4GWithFinishedRequest) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionWifi,
+ NetworkResponseType::Asynchronous);
+
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ EXPECT_EQ(nullptr, verifier.access_token_fetcher());
+
+ // Waiting for the network type returns.
+ SpinCurrentSequenceTaskRunner();
+
+ // The network type if wifi, send the request.
+ EXPECT_NE(nullptr, verifier.access_token_fetcher());
+
+ // Finishes the request.
+ identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
+ account_info.account_id, /*token=*/"", base::Time());
+ EXPECT_EQ(nullptr, verifier.access_token_fetcher());
+
+ // Network is changed to 4G.
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::Connection4G,
+ NetworkResponseType::Undecided);
+
+ // No more request because it's verfied already.
+ EXPECT_EQ(nullptr, verifier.access_token_fetcher());
+}
+
+// Regression test for https://crbug.com/1259864
+TEST(ForceSigninVerifierTest, DeleteWithPendingRequestShouldNotCrash) {
+ base::test::TaskEnvironment scoped_task_env;
+ signin::IdentityTestEnvironment identity_test_env;
+ const AccountInfo account_info =
+ identity_test_env.MakePrimaryAccountAvailable(
+ "email@test.com", signin::ConsentLevel::kSync);
+
+ ConfigureNetworkConnectionTracker(NetworkConnectionType::Undecided,
+ NetworkResponseType::Asynchronous);
+
+ {
+ ForceSigninVerifierWithAccessToInternalsForTesting verifier(
+ identity_test_env.identity_manager());
+
+ // There is no network type at first.
+ ASSERT_EQ(nullptr, verifier.access_token_fetcher());
+
+ // Delete the verifier while the request is pending.
+ }
+
+ // Waiting for the network type returns, this should not crash.
+ SpinCurrentSequenceTaskRunner();
+}
diff --git a/chromium/chrome/browser/signin/header_modification_delegate.h b/chromium/chrome/browser/signin/header_modification_delegate.h
new file mode 100644
index 00000000000..8d3bce133ac
--- /dev/null
+++ b/chromium/chrome/browser/signin/header_modification_delegate.h
@@ -0,0 +1,38 @@
+// Copyright 2018 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_SIGNIN_HEADER_MODIFICATION_DELEGATE_H_
+#define CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_H_
+
+class GURL;
+
+namespace content {
+class WebContents;
+}
+
+namespace signin {
+
+class ChromeRequestAdapter;
+class ResponseAdapter;
+
+class HeaderModificationDelegate {
+ public:
+ HeaderModificationDelegate() = default;
+
+ HeaderModificationDelegate(const HeaderModificationDelegate&) = delete;
+ HeaderModificationDelegate& operator=(const HeaderModificationDelegate&) =
+ delete;
+
+ virtual ~HeaderModificationDelegate() = default;
+
+ virtual bool ShouldInterceptNavigation(content::WebContents* contents) = 0;
+ virtual void ProcessRequest(ChromeRequestAdapter* request_adapter,
+ const GURL& redirect_url) = 0;
+ virtual void ProcessResponse(ResponseAdapter* response_adapter,
+ const GURL& redirect_url) = 0;
+};
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_H_
diff --git a/chromium/chrome/browser/signin/header_modification_delegate_impl.cc b/chromium/chrome/browser/signin/header_modification_delegate_impl.cc
new file mode 100644
index 00000000000..6aa2d10614f
--- /dev/null
+++ b/chromium/chrome/browser/signin/header_modification_delegate_impl.cc
@@ -0,0 +1,154 @@
+// Copyright 2018 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.
+
+#include "chrome/browser/signin/header_modification_delegate_impl.h"
+
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/content_settings/cookie_settings_factory.h"
+#include "chrome/browser/extensions/api/identity/web_auth_flow.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/chrome_signin_helper.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/sync/sync_service_factory.h"
+#include "chrome/common/pref_names.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/account_info.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/tribool.h"
+#include "components/sync/base/pref_names.h"
+#include "components/sync/driver/sync_service.h"
+#include "content/public/browser/render_process_host.h"
+#include "content/public/browser/site_instance.h"
+
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+#include "extensions/browser/guest_view/web_view/web_view_guest.h"
+#include "extensions/browser/guest_view/web_view/web_view_renderer_state.h"
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "components/account_manager_core/pref_names.h"
+#endif
+
+namespace signin {
+
+#if defined(OS_ANDROID)
+HeaderModificationDelegateImpl::HeaderModificationDelegateImpl(
+ Profile* profile,
+ bool incognito_enabled)
+ : profile_(profile),
+ cookie_settings_(CookieSettingsFactory::GetForProfile(profile_)),
+ incognito_enabled_(incognito_enabled) {}
+#else
+HeaderModificationDelegateImpl::HeaderModificationDelegateImpl(Profile* profile)
+ : profile_(profile),
+ cookie_settings_(CookieSettingsFactory::GetForProfile(profile_)) {}
+#endif
+
+HeaderModificationDelegateImpl::~HeaderModificationDelegateImpl() = default;
+
+bool HeaderModificationDelegateImpl::ShouldInterceptNavigation(
+ content::WebContents* contents) {
+ if (profile_->IsOffTheRecord())
+ return false;
+
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+ if (ShouldIgnoreGuestWebViewRequest(contents))
+ return false;
+#endif
+
+ return true;
+}
+
+void HeaderModificationDelegateImpl::ProcessRequest(
+ ChromeRequestAdapter* request_adapter,
+ const GURL& redirect_url) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ const PrefService* prefs = profile_->GetPrefs();
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ syncer::SyncService* sync_service =
+ SyncServiceFactory::GetForProfile(profile_);
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ bool is_secondary_account_addition_allowed = true;
+ if (!prefs->GetBoolean(
+ ::account_manager::prefs::kSecondaryGoogleAccountSigninAllowed)) {
+ is_secondary_account_addition_allowed = false;
+ }
+#endif
+
+ ConsentLevel consent_level = ConsentLevel::kSync;
+#if defined(OS_ANDROID)
+ consent_level = ConsentLevel::kSignin;
+#endif
+
+ IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile_);
+ CoreAccountInfo account =
+ identity_manager->GetPrimaryAccountInfo(consent_level);
+ signin::Tribool is_child_account =
+ // Defaults to kUnknown if the account is not found.
+ identity_manager->FindExtendedAccountInfo(account).is_child_account;
+
+ int incognito_mode_availability =
+ prefs->GetInteger(prefs::kIncognitoModeAvailability);
+#if defined(OS_ANDROID)
+ incognito_mode_availability =
+ incognito_enabled_
+ ? incognito_mode_availability
+ : static_cast<int>(IncognitoModePrefs::Availability::kDisabled);
+#endif
+
+ FixAccountConsistencyRequestHeader(
+ request_adapter, redirect_url, profile_->IsOffTheRecord(),
+ incognito_mode_availability,
+ AccountConsistencyModeManager::GetMethodForProfile(profile_),
+ account.gaia, is_child_account,
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ is_secondary_account_addition_allowed,
+#endif
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ sync_service && sync_service->IsSyncFeatureEnabled(),
+ prefs->GetString(prefs::kGoogleServicesSigninScopedDeviceId),
+#endif
+ cookie_settings_.get());
+}
+
+void HeaderModificationDelegateImpl::ProcessResponse(
+ ResponseAdapter* response_adapter,
+ const GURL& redirect_url) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ ProcessAccountConsistencyResponseHeaders(response_adapter, redirect_url,
+ profile_->IsOffTheRecord());
+}
+
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+// static
+bool HeaderModificationDelegateImpl::ShouldIgnoreGuestWebViewRequest(
+ content::WebContents* contents) {
+ if (!contents)
+ return true;
+
+ if (extensions::WebViewRendererState::GetInstance()->IsGuest(
+ contents->GetMainFrame()->GetProcess()->GetID())) {
+ auto identity_api_config =
+ extensions::WebAuthFlow::GetWebViewPartitionConfig(
+ extensions::WebAuthFlow::GET_AUTH_TOKEN,
+ contents->GetBrowserContext());
+ if (contents->GetSiteInstance()->GetStoragePartitionConfig() !=
+ identity_api_config)
+ return true;
+
+ // If the StoragePartitionConfig matches, but |contents| is not using a
+ // guest SiteInstance, then there is likely a serious bug.
+ CHECK(contents->GetSiteInstance()->IsGuest());
+ }
+ return false;
+}
+#endif
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/header_modification_delegate_impl.h b/chromium/chrome/browser/signin/header_modification_delegate_impl.h
new file mode 100644
index 00000000000..8bc1fd7fb2d
--- /dev/null
+++ b/chromium/chrome/browser/signin/header_modification_delegate_impl.h
@@ -0,0 +1,68 @@
+// Copyright 2018 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_SIGNIN_HEADER_MODIFICATION_DELEGATE_IMPL_H_
+#define CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_IMPL_H_
+
+#include "base/memory/raw_ptr.h"
+#include "build/build_config.h"
+#include "build/buildflag.h"
+#include "chrome/browser/signin/header_modification_delegate.h"
+#include "components/content_settings/core/browser/cookie_settings.h"
+#include "content/public/browser/browser_thread.h"
+#include "extensions/buildflags/buildflags.h"
+
+class Profile;
+
+namespace signin {
+
+// This class wraps the FixAccountConsistencyRequestHeader and
+// ProcessAccountConsistencyResponseHeaders in the HeaderModificationDelegate
+// interface.
+class HeaderModificationDelegateImpl : public HeaderModificationDelegate {
+ public:
+#if defined(OS_ANDROID)
+ explicit HeaderModificationDelegateImpl(Profile* profile,
+ bool incognito_enabled);
+#else
+ explicit HeaderModificationDelegateImpl(Profile* profile);
+#endif
+
+ HeaderModificationDelegateImpl(const HeaderModificationDelegateImpl&) =
+ delete;
+ HeaderModificationDelegateImpl& operator=(
+ const HeaderModificationDelegateImpl&) = delete;
+
+ ~HeaderModificationDelegateImpl() override;
+
+ // HeaderModificationDelegate
+ bool ShouldInterceptNavigation(content::WebContents* contents) override;
+ void ProcessRequest(ChromeRequestAdapter* request_adapter,
+ const GURL& redirect_url) override;
+ void ProcessResponse(ResponseAdapter* response_adapter,
+ const GURL& redirect_url) override;
+
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+ // Returns true if the request comes from a web view and should be ignored
+ // (i.e. not intercepted).
+ // Returns false if the request does not come from a web view.
+ // Requests coming from most guest web views are ignored. In particular the
+ // requests coming from the InlineLoginUI are not intercepted (see
+ // http://crbug.com/428396). Requests coming from the chrome identity
+ // extension consent flow are not ignored.
+ static bool ShouldIgnoreGuestWebViewRequest(content::WebContents* contents);
+#endif
+
+ private:
+ raw_ptr<Profile> profile_;
+ scoped_refptr<content_settings::CookieSettings> cookie_settings_;
+
+#if defined(OS_ANDROID)
+ bool incognito_enabled_;
+#endif
+};
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_IMPL_H_
diff --git a/chromium/chrome/browser/signin/identity_manager_factory.cc b/chromium/chrome/browser/signin/identity_manager_factory.cc
new file mode 100644
index 00000000000..cac771bf1e2
--- /dev/null
+++ b/chromium/chrome/browser/signin/identity_manager_factory.cc
@@ -0,0 +1,171 @@
+// Copyright 2017 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.
+
+#include "chrome/browser/signin/identity_manager_factory.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/files/file_path.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/image_fetcher/image_decoder_impl.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/identity_manager_provider.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/signin/public/base/signin_buildflags.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_manager_builder.h"
+#include "components/signin/public/webdata/token_web_data.h"
+#include "content/public/browser/network_service_instance.h"
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+#include "chrome/browser/content_settings/cookie_settings_factory.h"
+#include "chrome/browser/web_data_service_factory.h"
+#include "components/content_settings/core/browser/cookie_settings.h"
+#include "components/keyed_service/core/service_access_type.h"
+#include "components/signin/core/browser/cookie_settings_util.h"
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/ash/profiles/profile_helper.h"
+#include "chrome/browser/browser_process_platform_part.h"
+#include "components/account_manager_core/chromeos/account_manager_facade_factory.h"
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+#include "chrome/browser/lacros/account_manager/profile_account_manager.h"
+#include "chrome/browser/lacros/account_manager/profile_account_manager_factory.h"
+#include "components/account_manager_core/chromeos/account_manager_facade_factory.h"
+#endif
+
+#if defined(OS_WIN)
+#include "base/bind.h"
+#include "chrome/browser/signin/signin_util_win.h"
+#endif
+
+void IdentityManagerFactory::RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) {
+ signin::IdentityManager::RegisterProfilePrefs(registry);
+}
+
+IdentityManagerFactory::IdentityManagerFactory()
+ : BrowserContextKeyedServiceFactory(
+ "IdentityManager",
+ BrowserContextDependencyManager::GetInstance()) {
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ DependsOn(WebDataServiceFactory::GetInstance());
+#endif
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+ DependsOn(ProfileAccountManagerFactory::GetInstance());
+#endif
+ DependsOn(ChromeSigninClientFactory::GetInstance());
+ signin::SetIdentityManagerProvider(
+ base::BindRepeating([](content::BrowserContext* context) {
+ return GetForProfile(Profile::FromBrowserContext(context));
+ }));
+}
+
+IdentityManagerFactory::~IdentityManagerFactory() {
+ signin::SetIdentityManagerProvider({});
+}
+
+// static
+signin::IdentityManager* IdentityManagerFactory::GetForProfile(
+ Profile* profile) {
+ return static_cast<signin::IdentityManager*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+signin::IdentityManager* IdentityManagerFactory::GetForProfileIfExists(
+ const Profile* profile) {
+ return static_cast<signin::IdentityManager*>(
+ GetInstance()->GetServiceForBrowserContext(const_cast<Profile*>(profile),
+ false));
+}
+
+// static
+IdentityManagerFactory* IdentityManagerFactory::GetInstance() {
+ return base::Singleton<IdentityManagerFactory>::get();
+}
+
+// static
+void IdentityManagerFactory::EnsureFactoryAndDependeeFactoriesBuilt() {
+ IdentityManagerFactory::GetInstance();
+ ChromeSigninClientFactory::GetInstance();
+}
+
+void IdentityManagerFactory::AddObserver(Observer* observer) {
+ observer_list_.AddObserver(observer);
+}
+
+void IdentityManagerFactory::RemoveObserver(Observer* observer) {
+ observer_list_.RemoveObserver(observer);
+}
+
+KeyedService* IdentityManagerFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+
+ signin::IdentityManagerBuildParams params;
+ params.account_consistency =
+ AccountConsistencyModeManager::GetMethodForProfile(profile),
+ params.image_decoder = std::make_unique<ImageDecoderImpl>();
+ params.local_state = g_browser_process->local_state();
+ params.network_connection_tracker = content::GetNetworkConnectionTracker();
+ params.pref_service = profile->GetPrefs();
+ params.profile_path = profile->GetPath();
+ params.signin_client = ChromeSigninClientFactory::GetForProfile(profile);
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ params.delete_signin_cookies_on_exit =
+ signin::SettingsDeleteSigninCookiesOnExit(
+ CookieSettingsFactory::GetForProfile(profile).get());
+ params.token_web_data = WebDataServiceFactory::GetTokenWebDataForProfile(
+ profile, ServiceAccessType::EXPLICIT_ACCESS);
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ params.account_manager_facade =
+ GetAccountManagerFacade(profile->GetPath().value());
+ params.is_regular_profile =
+ chromeos::ProfileHelper::IsRegularProfile(profile);
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+ // The system and (original profile of the) guest profiles are not regular.
+ const bool is_regular_profile = profile->IsRegularProfile();
+ const bool use_profile_account_manager =
+ is_regular_profile &&
+ // `ProfileManager` may be null in tests, and is required for account
+ // consistency.
+ g_browser_process->profile_manager();
+
+ params.account_manager_facade =
+ use_profile_account_manager
+ ? ProfileAccountManagerFactory::GetForProfile(profile)
+ : GetAccountManagerFacade(profile->GetPath().value());
+ params.is_regular_profile = is_regular_profile;
+#endif
+
+#if defined(OS_WIN)
+ params.reauth_callback =
+ base::BindRepeating(&signin_util::ReauthWithCredentialProviderIfPossible,
+ base::Unretained(profile));
+#endif
+
+ std::unique_ptr<signin::IdentityManager> identity_manager =
+ signin::BuildIdentityManager(&params);
+
+ for (Observer& observer : observer_list_)
+ observer.IdentityManagerCreated(identity_manager.get());
+
+ return identity_manager.release();
+}
diff --git a/chromium/chrome/browser/signin/identity_manager_factory.h b/chromium/chrome/browser/signin/identity_manager_factory.h
new file mode 100644
index 00000000000..a2e0590da42
--- /dev/null
+++ b/chromium/chrome/browser/signin/identity_manager_factory.h
@@ -0,0 +1,67 @@
+// Copyright 2017 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_SIGNIN_IDENTITY_MANAGER_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "base/observer_list.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+namespace signin {
+class IdentityManager;
+}
+
+class Profile;
+
+// Singleton that owns all IdentityManager instances and associates them with
+// Profiles.
+class IdentityManagerFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ class Observer : public base::CheckedObserver {
+ public:
+ // Called when a IdentityManager instance is created.
+ virtual void IdentityManagerCreated(
+ signin::IdentityManager* identity_manager) {}
+
+ protected:
+ ~Observer() override {}
+ };
+
+ static signin::IdentityManager* GetForProfile(Profile* profile);
+ static signin::IdentityManager* GetForProfileIfExists(const Profile* profile);
+
+ // Returns an instance of the IdentityManagerFactory singleton.
+ static IdentityManagerFactory* GetInstance();
+
+ IdentityManagerFactory(const IdentityManagerFactory&) = delete;
+ IdentityManagerFactory& operator=(const IdentityManagerFactory&) = delete;
+
+ // Ensures that IdentityManagerFactory and the factories on which it depends
+ // are built.
+ static void EnsureFactoryAndDependeeFactoriesBuilt();
+
+ // Methods to register or remove observers of IdentityManager
+ // creation/shutdown.
+ void AddObserver(Observer* observer);
+ void RemoveObserver(Observer* observer);
+
+ private:
+ friend struct base::DefaultSingletonTraits<IdentityManagerFactory>;
+
+ IdentityManagerFactory();
+ ~IdentityManagerFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+ void RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) override;
+
+ // List of observers. Checks that list is empty on destruction.
+ base::ObserverList<Observer, /*check_empty=*/true, /*allow_reentrancy=*/false>
+ observer_list_;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/identity_manager_provider.cc b/chromium/chrome/browser/signin/identity_manager_provider.cc
new file mode 100644
index 00000000000..ca42e587adf
--- /dev/null
+++ b/chromium/chrome/browser/signin/identity_manager_provider.cc
@@ -0,0 +1,38 @@
+// Copyright 2021 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.
+
+#include "chrome/browser/signin/identity_manager_provider.h"
+
+#include "base/check.h"
+#include "base/no_destructor.h"
+
+namespace signin {
+
+namespace {
+
+IdentityManagerProvider& GetIdentityManagerProvider() {
+ static base::NoDestructor<IdentityManagerProvider> provider;
+ return *provider;
+}
+
+} // namespace
+
+void SetIdentityManagerProvider(const IdentityManagerProvider& provider) {
+ IdentityManagerProvider& instance = GetIdentityManagerProvider();
+
+ // Exactly one of `provider` or `instance` should be non-null.
+ if (provider)
+ DCHECK(!instance);
+ else
+ DCHECK(instance);
+
+ instance = provider;
+}
+
+IdentityManager* GetIdentityManagerForBrowserContext(
+ content::BrowserContext* context) {
+ return GetIdentityManagerProvider().Run(context);
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/identity_manager_provider.h b/chromium/chrome/browser/signin/identity_manager_provider.h
new file mode 100644
index 00000000000..9b6291d643b
--- /dev/null
+++ b/chromium/chrome/browser/signin/identity_manager_provider.h
@@ -0,0 +1,32 @@
+// Copyright 2021 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_SIGNIN_IDENTITY_MANAGER_PROVIDER_H_
+#define CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_PROVIDER_H_
+
+#include "base/callback.h"
+
+namespace content {
+class BrowserContext;
+}
+
+namespace signin {
+
+class IdentityManager;
+
+using IdentityManagerProvider =
+ base::RepeatingCallback<IdentityManager*(content::BrowserContext*)>;
+
+// Called by IdentityManagerFactory to expose a way to retrieve the
+// IdentityManager for a specific BrowserContext/Profile. This exists so that
+// components which don't depend on //chrome/browser can still access the
+// IdentityManager.
+void SetIdentityManagerProvider(const IdentityManagerProvider& provider);
+
+IdentityManager* GetIdentityManagerForBrowserContext(
+ content::BrowserContext* context);
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_PROVIDER_H_
diff --git a/chromium/chrome/browser/signin/identity_services_provider_android.cc b/chromium/chrome/browser/signin/identity_services_provider_android.cc
new file mode 100644
index 00000000000..cb49ede8c7d
--- /dev/null
+++ b/chromium/chrome/browser/signin/identity_services_provider_android.cc
@@ -0,0 +1,39 @@
+// Copyright 2018 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.
+
+#include "base/android/jni_android.h"
+#include "chrome/browser/profiles/profile_android.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/services/android/jni_headers/IdentityServicesProvider_jni.h"
+#include "chrome/browser/signin/signin_manager_android_factory.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+
+using base::android::JavaParamRef;
+using base::android::ScopedJavaLocalRef;
+
+static ScopedJavaLocalRef<jobject>
+JNI_IdentityServicesProvider_GetIdentityManager(
+ JNIEnv* env,
+ const JavaParamRef<jobject>& j_profile_android) {
+ Profile* profile = ProfileAndroid::FromProfileAndroid(j_profile_android);
+ return IdentityManagerFactory::GetForProfile(profile)->GetJavaObject();
+}
+
+static ScopedJavaLocalRef<jobject>
+JNI_IdentityServicesProvider_GetAccountTrackerService(
+ JNIEnv* env,
+ const JavaParamRef<jobject>& j_profile_android) {
+ Profile* profile = ProfileAndroid::FromProfileAndroid(j_profile_android);
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ return identity_manager->LegacyGetAccountTrackerServiceJavaObject();
+}
+
+static ScopedJavaLocalRef<jobject>
+JNI_IdentityServicesProvider_GetSigninManager(
+ JNIEnv* env,
+ const JavaParamRef<jobject>& j_profile_android) {
+ Profile* profile = ProfileAndroid::FromProfileAndroid(j_profile_android);
+ return SigninManagerAndroidFactory::GetJavaObjectForProfile(profile);
+}
diff --git a/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.cc b/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.cc
new file mode 100644
index 00000000000..ff94a971878
--- /dev/null
+++ b/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.cc
@@ -0,0 +1,103 @@
+// Copyright 2018 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.
+
+#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
+
+#include "base/bind.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/signin/chrome_signin_client_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "ash/components/account_manager/account_manager_factory.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/browser_process_platform_part.h"
+#include "components/account_manager_core/chromeos/account_manager_facade_factory.h"
+#endif
+
+// static
+std::unique_ptr<TestingProfile> IdentityTestEnvironmentProfileAdaptor::
+ CreateProfileForIdentityTestEnvironment() {
+ return CreateProfileForIdentityTestEnvironment(
+ TestingProfile::TestingFactories());
+}
+
+// static
+std::unique_ptr<TestingProfile>
+IdentityTestEnvironmentProfileAdaptor::CreateProfileForIdentityTestEnvironment(
+ const TestingProfile::TestingFactories& input_factories) {
+ TestingProfile::Builder builder;
+
+ for (auto& input_factory : input_factories) {
+ builder.AddTestingFactory(input_factory.first, input_factory.second);
+ }
+
+ return CreateProfileForIdentityTestEnvironment(builder);
+}
+
+// static
+std::unique_ptr<TestingProfile>
+IdentityTestEnvironmentProfileAdaptor::CreateProfileForIdentityTestEnvironment(
+ TestingProfile::Builder& builder,
+ signin::AccountConsistencyMethod account_consistency) {
+ for (auto& identity_factory :
+ GetIdentityTestEnvironmentFactories(account_consistency)) {
+ builder.AddTestingFactory(identity_factory.first, identity_factory.second);
+ }
+
+ return builder.Build();
+}
+
+// static
+void IdentityTestEnvironmentProfileAdaptor::
+ SetIdentityTestEnvironmentFactoriesOnBrowserContext(
+ content::BrowserContext* context) {
+ for (const auto& factory_pair : GetIdentityTestEnvironmentFactories()) {
+ factory_pair.first->SetTestingFactory(context, factory_pair.second);
+ }
+}
+
+// static
+void IdentityTestEnvironmentProfileAdaptor::
+ AppendIdentityTestEnvironmentFactories(
+ TestingProfile::TestingFactories* factories_to_append_to) {
+ TestingProfile::TestingFactories identity_factories =
+ GetIdentityTestEnvironmentFactories();
+ factories_to_append_to->insert(factories_to_append_to->end(),
+ identity_factories.begin(),
+ identity_factories.end());
+}
+
+// static
+TestingProfile::TestingFactories
+IdentityTestEnvironmentProfileAdaptor::GetIdentityTestEnvironmentFactories(
+ signin::AccountConsistencyMethod account_consistency) {
+ return {{IdentityManagerFactory::GetInstance(),
+ base::BindRepeating(&BuildIdentityManagerForTests,
+ account_consistency)}};
+}
+
+// static
+std::unique_ptr<KeyedService>
+IdentityTestEnvironmentProfileAdaptor::BuildIdentityManagerForTests(
+ signin::AccountConsistencyMethod account_consistency,
+ content::BrowserContext* context) {
+ Profile* profile = Profile::FromBrowserContext(context);
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ return signin::IdentityTestEnvironment::BuildIdentityManagerForTests(
+ ChromeSigninClientFactory::GetForProfile(profile), profile->GetPrefs(),
+ profile->GetPath(),
+ g_browser_process->platform_part()->GetAccountManagerFactory(),
+ GetAccountManagerFacade(profile->GetPath().value()));
+#else
+ return signin::IdentityTestEnvironment::BuildIdentityManagerForTests(
+ ChromeSigninClientFactory::GetForProfile(profile), profile->GetPrefs(),
+ profile->GetPath(), account_consistency);
+#endif
+}
+
+IdentityTestEnvironmentProfileAdaptor::IdentityTestEnvironmentProfileAdaptor(
+ Profile* profile)
+ : identity_test_env_(IdentityManagerFactory::GetForProfile(profile),
+ ChromeSigninClientFactory::GetForProfile(profile)) {}
diff --git a/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.h b/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.h
new file mode 100644
index 00000000000..ef477efd9d3
--- /dev/null
+++ b/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.h
@@ -0,0 +1,101 @@
+// Copyright 2018 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_SIGNIN_IDENTITY_TEST_ENVIRONMENT_PROFILE_ADAPTOR_H_
+#define CHROME_BROWSER_SIGNIN_IDENTITY_TEST_ENVIRONMENT_PROFILE_ADAPTOR_H_
+
+#include "chrome/test/base/testing_profile.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "components/sync_preferences/testing_pref_service_syncable.h"
+
+// Adaptor that supports signin::IdentityTestEnvironment's usage in testing
+// contexts where the relevant fake objects must be injected via the
+// BrowserContextKeyedServiceFactory infrastructure as the production code
+// accesses IdentityManager via that infrastructure. Before using this
+// class, please consider whether you can change the production code in question
+// to take in the relevant dependencies directly rather than obtaining them from
+// the Profile; this is both cleaner in general and allows for direct usage of
+// signin::IdentityTestEnvironment in the test.
+class IdentityTestEnvironmentProfileAdaptor {
+ public:
+ // Creates and returns a TestingProfile that has been configured with the set
+ // of testing factories that IdentityTestEnvironment requires.
+ static std::unique_ptr<TestingProfile>
+ CreateProfileForIdentityTestEnvironment();
+
+ // Like the above, but additionally configures the returned Profile with
+ // |input_factories|.
+ static std::unique_ptr<TestingProfile>
+ CreateProfileForIdentityTestEnvironment(
+ const TestingProfile::TestingFactories& input_factories);
+
+ // Creates and returns a TestingProfile that has been configured with the
+ // given |builder| and the set of testing factories that
+ // IdentityTestEnvironment requires.
+ // See the above variant for comments on common parameters.
+ static std::unique_ptr<TestingProfile>
+ CreateProfileForIdentityTestEnvironment(
+ TestingProfile::Builder& builder,
+ signin::AccountConsistencyMethod account_consistency =
+ signin::AccountConsistencyMethod::kDisabled);
+
+ // Sets the testing factories that signin::IdentityTestEnvironment
+ // requires explicitly on a Profile that is passed to it.
+ // See the above variant for comments on common parameters.
+ static void SetIdentityTestEnvironmentFactoriesOnBrowserContext(
+ content::BrowserContext* browser_context);
+
+ // Appends the set of testing factories that signin::IdentityTestEnvironment
+ // requires to |factories_to_append_to|, which should be the set of testing
+ // factories supplied to TestingProfile (via one of the various mechanisms for
+ // doing so). Prefer the above API if possible, as it is less fragile. This
+ // API is primarily for use in tests that do not create the TestingProfile
+ // internally but rather simply supply the set of TestingFactories to some
+ // external facility (e.g., a superclass).
+ // See CreateProfileForIdentityTestEnvironment() for comments on common
+ // parameters.
+ static void AppendIdentityTestEnvironmentFactories(
+ TestingProfile::TestingFactories* factories_to_append_to);
+
+ // Returns the set of testing factories that signin::IdentityTestEnvironment
+ // requires, which can be useful to configure profiles for services that do
+ // not require any other testing factory than the ones specified in here.
+ static TestingProfile::TestingFactories GetIdentityTestEnvironmentFactories(
+ signin::AccountConsistencyMethod account_consistency =
+ signin::AccountConsistencyMethod::kDisabled);
+
+ // Constructs an adaptor that associates an IdentityTestEnvironment instance
+ // with |profile| via the relevant backing objects. Note that
+ // |profile| must have been configured with the IdentityTestEnvironment
+ // testing factories, either because it was created via
+ // CreateProfileForIdentityTestEnvironment() or because
+ // AppendIdentityTestEnvironmentFactories() was invoked on the set of
+ // factories supplied to it.
+ // |profile| must outlive this object.
+ explicit IdentityTestEnvironmentProfileAdaptor(Profile* profile);
+
+ IdentityTestEnvironmentProfileAdaptor(
+ const IdentityTestEnvironmentProfileAdaptor&) = delete;
+ IdentityTestEnvironmentProfileAdaptor& operator=(
+ const IdentityTestEnvironmentProfileAdaptor&) = delete;
+
+ ~IdentityTestEnvironmentProfileAdaptor() {}
+
+ // Returns the IdentityTestEnvironment associated with this object (and
+ // implicitly with the Profile passed to this object's constructor).
+ signin::IdentityTestEnvironment* identity_test_env() {
+ return &identity_test_env_;
+ }
+
+ private:
+ // Testing factory that creates an IdentityManager
+ // with a FakeProfileOAuth2TokenService.
+ static std::unique_ptr<KeyedService> BuildIdentityManagerForTests(
+ signin::AccountConsistencyMethod account_consistency,
+ content::BrowserContext* context);
+
+ signin::IdentityTestEnvironment identity_test_env_;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_IDENTITY_TEST_ENVIRONMENT_PROFILE_ADAPTOR_H_
diff --git a/chromium/chrome/browser/signin/investigator_dependency_provider.cc b/chromium/chrome/browser/signin/investigator_dependency_provider.cc
new file mode 100644
index 00000000000..601806d2669
--- /dev/null
+++ b/chromium/chrome/browser/signin/investigator_dependency_provider.cc
@@ -0,0 +1,14 @@
+// Copyright 2015 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.
+
+#include "chrome/browser/signin/investigator_dependency_provider.h"
+
+InvestigatorDependencyProvider::InvestigatorDependencyProvider(Profile* profile)
+ : profile_(profile) {}
+
+InvestigatorDependencyProvider::~InvestigatorDependencyProvider() {}
+
+PrefService* InvestigatorDependencyProvider::GetPrefs() {
+ return profile_->GetPrefs();
+}
diff --git a/chromium/chrome/browser/signin/investigator_dependency_provider.h b/chromium/chrome/browser/signin/investigator_dependency_provider.h
new file mode 100644
index 00000000000..199f7bb07a5
--- /dev/null
+++ b/chromium/chrome/browser/signin/investigator_dependency_provider.h
@@ -0,0 +1,33 @@
+// Copyright 2015 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_SIGNIN_INVESTIGATOR_DEPENDENCY_PROVIDER_H_
+#define CHROME_BROWSER_SIGNIN_INVESTIGATOR_DEPENDENCY_PROVIDER_H_
+
+#include "base/memory/raw_ptr.h"
+#include "chrome/browser/profiles/profile.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/core/browser/signin_investigator.h"
+
+// This version should work for anything with a profile object, like desktop and
+// Android.
+class InvestigatorDependencyProvider
+ : public SigninInvestigator::DependencyProvider {
+ public:
+ explicit InvestigatorDependencyProvider(Profile* profile);
+
+ InvestigatorDependencyProvider(const InvestigatorDependencyProvider&) =
+ delete;
+ InvestigatorDependencyProvider& operator=(
+ const InvestigatorDependencyProvider&) = delete;
+
+ ~InvestigatorDependencyProvider() override;
+ PrefService* GetPrefs() override;
+
+ private:
+ // Non-owning pointer.
+ raw_ptr<Profile> profile_;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_INVESTIGATOR_DEPENDENCY_PROVIDER_H_
diff --git a/chromium/chrome/browser/signin/logout_tab_helper.cc b/chromium/chrome/browser/signin/logout_tab_helper.cc
new file mode 100644
index 00000000000..db560b21a69
--- /dev/null
+++ b/chromium/chrome/browser/signin/logout_tab_helper.cc
@@ -0,0 +1,34 @@
+// Copyright 2020 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.
+
+#include "chrome/browser/signin/logout_tab_helper.h"
+
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "components/signin/public/identity_manager/accounts_mutator.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+
+WEB_CONTENTS_USER_DATA_KEY_IMPL(LogoutTabHelper);
+
+LogoutTabHelper::LogoutTabHelper(content::WebContents* web_contents)
+ : content::WebContentsUserData<LogoutTabHelper>(*web_contents),
+ content::WebContentsObserver(web_contents) {}
+
+LogoutTabHelper::~LogoutTabHelper() = default;
+
+void LogoutTabHelper::PrimaryPageChanged(content::Page& page) {
+ if (page.GetMainDocument().IsErrorDocument()) {
+ // Failed to load the logout page, fallback to local signout.
+ Profile* profile =
+ Profile::FromBrowserContext(web_contents()->GetBrowserContext());
+ IdentityManagerFactory::GetForProfile(profile)
+ ->GetAccountsMutator()
+ ->RemoveAllAccounts(signin_metrics::SourceForRefreshTokenOperation::
+ kLogoutTabHelper_PrimaryPageChanged);
+ }
+
+ // Delete this.
+ web_contents()->RemoveUserData(UserDataKey());
+}
diff --git a/chromium/chrome/browser/signin/logout_tab_helper.h b/chromium/chrome/browser/signin/logout_tab_helper.h
new file mode 100644
index 00000000000..4f2974c4568
--- /dev/null
+++ b/chromium/chrome/browser/signin/logout_tab_helper.h
@@ -0,0 +1,36 @@
+// Copyright 2020 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_SIGNIN_LOGOUT_TAB_HELPER_H_
+#define CHROME_BROWSER_SIGNIN_LOGOUT_TAB_HELPER_H_
+
+#include "content/public/browser/web_contents_observer.h"
+#include "content/public/browser/web_contents_user_data.h"
+
+// Tab helper used for logout tabs. Monitors if the logout tab loaded correctly
+// and fallbacks to local signout in case of failure.
+// Only the first navigation is monitored. Even though the logout page sometimes
+// redirects to the SAML provider through javascript, that second navigation is
+// not monitored. The logout is considered successful if the first navigation
+// succeeds, because the signout headers which cause the tokens to be revoked
+// are there.
+class LogoutTabHelper : public content::WebContentsUserData<LogoutTabHelper>,
+ public content::WebContentsObserver {
+ public:
+ ~LogoutTabHelper() override;
+
+ LogoutTabHelper(const LogoutTabHelper&) = delete;
+ LogoutTabHelper& operator=(const LogoutTabHelper&) = delete;
+
+ private:
+ friend class content::WebContentsUserData<LogoutTabHelper>;
+ explicit LogoutTabHelper(content::WebContents* web_contents);
+
+ // content::WebContentsObserver:
+ void PrimaryPageChanged(content::Page& page) override;
+
+ WEB_CONTENTS_USER_DATA_KEY_DECL();
+};
+
+#endif // CHROME_BROWSER_SIGNIN_LOGOUT_TAB_HELPER_H_
diff --git a/chromium/chrome/browser/signin/logout_tab_helper_unittest.cc b/chromium/chrome/browser/signin/logout_tab_helper_unittest.cc
new file mode 100644
index 00000000000..7717ade215c
--- /dev/null
+++ b/chromium/chrome/browser/signin/logout_tab_helper_unittest.cc
@@ -0,0 +1,24 @@
+// Copyright 2021 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.
+
+#include "chrome/browser/signin/logout_tab_helper.h"
+
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "content/public/test/navigation_simulator.h"
+#include "google_apis/gaia/gaia_urls.h"
+
+class LogoutTabHelperTest : public ChromeRenderViewHostTestHarness {};
+
+TEST_F(LogoutTabHelperTest, SelfDeleteInPrimaryPageChanged) {
+ LogoutTabHelper::CreateForWebContents(web_contents());
+
+ EXPECT_NE(nullptr, LogoutTabHelper::FromWebContents(web_contents()));
+
+ // Load the logout page.
+ content::NavigationSimulator::NavigateAndCommitFromBrowser(
+ web_contents(), GaiaUrls::GetInstance()->service_logout_url());
+
+ // The helper was deleted in PrimaryPageChanged.
+ EXPECT_EQ(nullptr, LogoutTabHelper::FromWebContents(web_contents()));
+}
diff --git a/chromium/chrome/browser/signin/mirror_browsertest.cc b/chromium/chrome/browser/signin/mirror_browsertest.cc
new file mode 100644
index 00000000000..abc65d011e6
--- /dev/null
+++ b/chromium/chrome/browser/signin/mirror_browsertest.cc
@@ -0,0 +1,277 @@
+// Copyright 2019 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.
+
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/base_switches.h"
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/callback_helpers.h"
+#include "base/command_line.h"
+#include "base/path_service.h"
+#include "base/run_loop.h"
+#include "base/task/post_task.h"
+#include "base/test/bind.h"
+#include "build/build_config.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/chrome_content_browser_client.h"
+#include "chrome/browser/extensions/api/identity/web_auth_flow.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/signin_util.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/common/chrome_paths.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/google/core/common/google_switches.h"
+#include "components/network_session_configurator/common/network_switches.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/core/browser/dice_header_helper.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "content/public/common/content_client.h"
+#include "content/public/test/browser_test.h"
+#include "google_apis/gaia/gaia_urls.h"
+#include "net/dns/mock_host_resolver.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/test/embedded_test_server/request_handler_util.h"
+#include "third_party/blink/public/common/loader/url_loader_throttle.h"
+
+namespace {
+
+// A delegate to insert a user generated X-Chrome-Connected header
+// to a specifict URL.
+class HeaderModifyingThrottle : public blink::URLLoaderThrottle {
+ public:
+ HeaderModifyingThrottle() = default;
+
+ HeaderModifyingThrottle(const HeaderModifyingThrottle&) = delete;
+ HeaderModifyingThrottle& operator=(const HeaderModifyingThrottle&) = delete;
+
+ ~HeaderModifyingThrottle() override = default;
+
+ void WillStartRequest(network::ResourceRequest* request,
+ bool* defer) override {
+ request->headers.SetHeader(signin::kChromeConnectedHeader, "User Data");
+ }
+};
+
+class ThrottleContentBrowserClient : public ChromeContentBrowserClient {
+ public:
+ explicit ThrottleContentBrowserClient(const GURL& watch_url)
+ : watch_url_(watch_url) {}
+
+ ThrottleContentBrowserClient(const ThrottleContentBrowserClient&) = delete;
+ ThrottleContentBrowserClient& operator=(const ThrottleContentBrowserClient&) =
+ delete;
+
+ ~ThrottleContentBrowserClient() override = default;
+
+ // ContentBrowserClient overrides:
+ std::vector<std::unique_ptr<blink::URLLoaderThrottle>>
+ CreateURLLoaderThrottles(
+ const network::ResourceRequest& request,
+ content::BrowserContext* browser_context,
+ const base::RepeatingCallback<content::WebContents*()>& wc_getter,
+ content::NavigationUIData* navigation_ui_data,
+ int frame_tree_node_id) override {
+ std::vector<std::unique_ptr<blink::URLLoaderThrottle>> throttles;
+ if (request.url == watch_url_)
+ throttles.push_back(std::make_unique<HeaderModifyingThrottle>());
+ return throttles;
+ }
+
+ private:
+ const GURL watch_url_;
+};
+
+// Subclass of DiceManageAccountBrowserTest with Mirror enabled.
+class MirrorBrowserTest : public InProcessBrowserTest {
+ protected:
+ void RunExtensionConsentTest(extensions::WebAuthFlow::Partition partition,
+ bool expects_header) {
+ net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
+ https_server.AddDefaultHandlers(GetChromeTestDataDir());
+ const std::string kAuthPath = "/auth";
+ net::test_server::HttpRequest::HeaderMap headers;
+ base::RunLoop run_loop;
+ https_server.RegisterRequestMonitor(base::BindLambdaForTesting(
+ [&](const net::test_server::HttpRequest& request) {
+ if (request.GetURL().path() != kAuthPath)
+ return;
+
+ headers = request.headers;
+ run_loop.Quit();
+ }));
+ ASSERT_TRUE(https_server.Start());
+
+ auto web_auth_flow = std::make_unique<extensions::WebAuthFlow>(
+ nullptr, browser()->profile(),
+ https_server.GetURL("google.com", kAuthPath),
+ extensions::WebAuthFlow::INTERACTIVE, partition);
+
+ web_auth_flow->Start();
+ run_loop.Run();
+ EXPECT_EQ(!!headers.count(signin::kChromeConnectedHeader), expects_header);
+
+ web_auth_flow.release()->DetachDelegateAndDelete();
+ base::RunLoop().RunUntilIdle();
+ }
+
+ private:
+ void SetUpOnMainThread() override {
+ // The test makes requests to google.com and other domains which we want to
+ // redirect to the test server.
+ host_resolver()->AddRule("*", "127.0.0.1");
+ }
+
+ void SetUpCommandLine(base::CommandLine* command_line) override {
+ // HTTPS server only serves a valid cert for localhost, so this is needed to
+ // load pages from "www.google.com" without an interstitial.
+ command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
+
+ // The production code only allows known ports (80 for http and 443 for
+ // https), but the test server runs on a random port.
+ command_line->AppendSwitch(switches::kIgnoreGooglePortNumbers);
+ }
+};
+
+// Verify the following items:
+// 1- X-Chrome-Connected is appended on Google domains if account
+// consistency is enabled and access is secure.
+// 2- The header is stripped in case a request is redirected from a Gooogle
+// domain to non-google domain.
+// 3- The header is NOT stripped in case it is added directly by the page
+// and not because it was on a secure Google domain.
+// This is a regression test for crbug.com/588492.
+IN_PROC_BROWSER_TEST_F(MirrorBrowserTest, MirrorRequestHeader) {
+ browser()->profile()->GetPrefs()->SetString(prefs::kGoogleServicesAccountId,
+ "account_id");
+
+ base::Lock lock;
+ // Map from the path of the URLs that test server sees to the request header.
+ // This is the path, and not URL, because the requests use different domains
+ // which the mock HostResolver converts to 127.0.0.1.
+ std::map<std::string, net::test_server::HttpRequest::HeaderMap> header_map;
+ embedded_test_server()->RegisterRequestMonitor(base::BindLambdaForTesting(
+ [&](const net::test_server::HttpRequest& request) {
+ base::AutoLock auto_lock(lock);
+ header_map[request.GetURL().path()] = request.headers;
+ }));
+ ASSERT_TRUE(embedded_test_server()->Start());
+
+ net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
+ https_server.AddDefaultHandlers(GetChromeTestDataDir());
+ https_server.RegisterRequestMonitor(base::BindLambdaForTesting(
+ [&](const net::test_server::HttpRequest& request) {
+ base::AutoLock auto_lock(lock);
+ header_map[request.GetURL().path()] = request.headers;
+ }));
+ ASSERT_TRUE(https_server.Start());
+
+ base::FilePath root_http;
+ base::PathService::Get(chrome::DIR_TEST_DATA, &root_http);
+ root_http = root_http.AppendASCII("mirror_request_header");
+
+ struct TestCase {
+ GURL original_url; // The URL from which the request begins.
+ // The path to which navigation is redirected.
+ std::string redirected_to_path;
+ bool inject_header; // Should X-Chrome-Connected header be injected to the
+ // original request.
+ bool original_url_expects_header; // Expectation: The header should be
+ // visible in original URL.
+ bool redirected_to_url_expects_header; // Expectation: The header should be
+ // visible in redirected URL.
+ };
+
+ std::vector<TestCase> all_tests;
+
+ // Neither should have the header.
+ // Note we need to replace the port of the redirect's URL.
+ base::StringPairs replacement_text;
+ replacement_text.push_back(std::make_pair(
+ "{{PORT}}", base::NumberToString(embedded_test_server()->port())));
+ std::string replacement_path = net::test_server::GetFilePathWithReplacements(
+ "/mirror_request_header/http.www.google.com.html", replacement_text);
+ all_tests.push_back(
+ {embedded_test_server()->GetURL("www.google.com", replacement_path),
+ "/simple.html", false, false, false});
+
+ // First one adds the header and transfers it to the second.
+ replacement_path = net::test_server::GetFilePathWithReplacements(
+ "/mirror_request_header/http.www.header_adder.com.html",
+ replacement_text);
+ all_tests.push_back(
+ {embedded_test_server()->GetURL("www.header_adder.com", replacement_path),
+ "/simple.html", true, true, true});
+
+ // First one should have the header, but not transfered to second one.
+ replacement_text.clear();
+ replacement_text.push_back(
+ std::make_pair("{{PORT}}", base::NumberToString(https_server.port())));
+ replacement_path = net::test_server::GetFilePathWithReplacements(
+ "/mirror_request_header/https.www.google.com.html", replacement_text);
+ all_tests.push_back({https_server.GetURL("www.google.com", replacement_path),
+ "/simple.html", false, true, false});
+
+ for (const auto& test_case : all_tests) {
+ SCOPED_TRACE(test_case.original_url);
+
+ // If test case requires adding header for the first url add a throttle.
+ ThrottleContentBrowserClient browser_client(test_case.original_url);
+ content::ContentBrowserClient* old_browser_client = nullptr;
+ if (test_case.inject_header)
+ old_browser_client = content::SetBrowserClientForTesting(&browser_client);
+
+ // Navigate to first url.
+ ASSERT_TRUE(
+ ui_test_utils::NavigateToURL(browser(), test_case.original_url));
+
+ if (test_case.inject_header)
+ content::SetBrowserClientForTesting(old_browser_client);
+
+ base::AutoLock auto_lock(lock);
+
+ // Check if header exists and X-Chrome-Connected is correctly provided.
+ ASSERT_EQ(1u, header_map.count(test_case.original_url.path()));
+ if (test_case.original_url_expects_header) {
+ ASSERT_TRUE(header_map[test_case.original_url.path()].count(
+ signin::kChromeConnectedHeader));
+ } else {
+ ASSERT_FALSE(header_map[test_case.original_url.path()].count(
+ signin::kChromeConnectedHeader));
+ }
+
+ ASSERT_EQ(1u, header_map.count(test_case.redirected_to_path));
+ if (test_case.redirected_to_url_expects_header) {
+ ASSERT_TRUE(header_map[test_case.redirected_to_path].count(
+ signin::kChromeConnectedHeader));
+ } else {
+ ASSERT_FALSE(header_map[test_case.redirected_to_path].count(
+ signin::kChromeConnectedHeader));
+ }
+
+ header_map.clear();
+ }
+}
+
+// Verifies that requests originated from chrome.identity.launchWebAuthFlow()
+// API don't have Mirror headers attached.
+// This is a regression test for crbug.com/1077504.
+IN_PROC_BROWSER_TEST_F(MirrorBrowserTest,
+ NoMirrorExtensionConsent_LaunchWebAuthFlow) {
+ RunExtensionConsentTest(extensions::WebAuthFlow::LAUNCH_WEB_AUTH_FLOW, false);
+}
+
+// Verifies that requests originated from chrome.identity.getAuthToken()
+// API have Mirror headers attached.
+IN_PROC_BROWSER_TEST_F(MirrorBrowserTest, MirrorExtensionConsent_GetAuthToken) {
+ RunExtensionConsentTest(extensions::WebAuthFlow::GET_AUTH_TOKEN, true);
+}
+
+} // namespace
diff --git a/chromium/chrome/browser/signin/process_dice_header_delegate_impl.cc b/chromium/chrome/browser/signin/process_dice_header_delegate_impl.cc
new file mode 100644
index 00000000000..5669e179b48
--- /dev/null
+++ b/chromium/chrome/browser/signin/process_dice_header_delegate_impl.cc
@@ -0,0 +1,146 @@
+// Copyright 2017 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.
+
+#include "chrome/browser/signin/process_dice_header_delegate_impl.h"
+
+#include <utility>
+
+#include "base/callback.h"
+#include "base/logging.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/dice_tab_helper.h"
+#include "chrome/browser/signin/dice_web_signin_interceptor.h"
+#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/ui/webui/signin/signin_ui_error.h"
+#include "chrome/common/url_constants.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "content/public/browser/navigation_controller.h"
+#include "content/public/browser/web_contents.h"
+#include "url/gurl.h"
+
+namespace {
+
+void RedirectToNtp(content::WebContents* contents) {
+ VLOG(1) << "RedirectToNtp";
+ contents->GetController().LoadURL(
+ GURL(chrome::kChromeUINewTabURL), content::Referrer(),
+ ui::PAGE_TRANSITION_AUTO_TOPLEVEL, std::string());
+}
+
+// Helper function similar to DiceTabHelper::FromWebContents(), but also handles
+// the case where |contents| is nullptr.
+DiceTabHelper* GetDiceTabHelperFromWebContents(content::WebContents* contents) {
+ if (!contents)
+ return nullptr;
+ return DiceTabHelper::FromWebContents(contents);
+}
+
+} // namespace
+
+ProcessDiceHeaderDelegateImpl::ProcessDiceHeaderDelegateImpl(
+ content::WebContents* web_contents,
+ EnableSyncCallback enable_sync_callback,
+ ShowSigninErrorCallback show_signin_error_callback)
+ : web_contents_(web_contents->GetWeakPtr()),
+ profile_(Profile::FromBrowserContext(web_contents->GetBrowserContext())),
+ enable_sync_callback_(std::move(enable_sync_callback)),
+ show_signin_error_callback_(std::move(show_signin_error_callback)) {
+ DCHECK(profile_);
+
+ DiceTabHelper* tab_helper = DiceTabHelper::FromWebContents(web_contents);
+ if (tab_helper) {
+ is_sync_signin_tab_ = tab_helper->IsSyncSigninInProgress();
+ redirect_url_ = tab_helper->redirect_url();
+ access_point_ = tab_helper->signin_access_point();
+ promo_action_ = tab_helper->signin_promo_action();
+ reason_ = tab_helper->signin_reason();
+ }
+}
+
+ProcessDiceHeaderDelegateImpl::~ProcessDiceHeaderDelegateImpl() = default;
+
+bool ProcessDiceHeaderDelegateImpl::ShouldEnableSync() {
+ if (IdentityManagerFactory::GetForProfile(profile_)->HasPrimaryAccount(
+ signin::ConsentLevel::kSync)) {
+ VLOG(1) << "Do not start sync after web sign-in [already authenticated].";
+ return false;
+ }
+
+ if (!is_sync_signin_tab_) {
+ VLOG(1)
+ << "Do not start sync after web sign-in [not a Chrome sign-in tab].";
+ return false;
+ }
+
+ return true;
+}
+
+void ProcessDiceHeaderDelegateImpl::HandleTokenExchangeSuccess(
+ CoreAccountId account_id,
+ bool is_new_account) {
+ // is_sync_signin_tab_ tells whether the current signin is happening in a tab
+ // that was opened from a "Enable Sync" Chrome UI. Usually this is indeed a
+ // sync signin, but it is not always the case: the user may abandon the sync
+ // signin and do a simple web signin in the same tab instead.
+ DiceWebSigninInterceptorFactory::GetForProfile(profile_)
+ ->MaybeInterceptWebSignin(web_contents_.get(), account_id, is_new_account,
+ is_sync_signin_tab_);
+}
+
+void ProcessDiceHeaderDelegateImpl::EnableSync(
+ const CoreAccountId& account_id) {
+ DiceTabHelper* tab_helper =
+ GetDiceTabHelperFromWebContents(web_contents_.get());
+ if (tab_helper)
+ tab_helper->OnSyncSigninFlowComplete();
+
+ if (!ShouldEnableSync()) {
+ // No special treatment is needed if the user is not enabling sync.
+ return;
+ }
+
+ content::WebContents* web_contents = web_contents_.get();
+ VLOG(1) << "Start sync after web sign-in.";
+ std::move(enable_sync_callback_)
+ .Run(profile_.get(), access_point_, promo_action_, reason_, web_contents,
+ account_id);
+
+ if (!web_contents)
+ return;
+
+ // After signing in to Chrome, the user should be redirected to the NTP,
+ // unless specified otherwise.
+ if (redirect_url_.is_empty()) {
+ RedirectToNtp(web_contents);
+ return;
+ }
+
+ DCHECK(redirect_url_.is_valid());
+ web_contents->GetController().LoadURL(redirect_url_, content::Referrer(),
+ ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
+ std::string());
+}
+
+void ProcessDiceHeaderDelegateImpl::HandleTokenExchangeFailure(
+ const std::string& email,
+ const GoogleServiceAuthError& error) {
+ DCHECK_NE(GoogleServiceAuthError::NONE, error.state());
+ DiceTabHelper* tab_helper =
+ GetDiceTabHelperFromWebContents(web_contents_.get());
+ if (tab_helper)
+ tab_helper->OnSyncSigninFlowComplete();
+
+ bool should_enable_sync = ShouldEnableSync();
+
+ content::WebContents* web_contents = web_contents_.get();
+ if (should_enable_sync && web_contents)
+ RedirectToNtp(web_contents);
+
+ // Show the error even if the WebContents was closed, because the user may be
+ // signed out of the web.
+ std::move(show_signin_error_callback_)
+ .Run(profile_.get(), web_contents,
+ SigninUIError::FromGoogleServiceAuthError(email, error));
+}
diff --git a/chromium/chrome/browser/signin/process_dice_header_delegate_impl.h b/chromium/chrome/browser/signin/process_dice_header_delegate_impl.h
new file mode 100644
index 00000000000..cd7116700c6
--- /dev/null
+++ b/chromium/chrome/browser/signin/process_dice_header_delegate_impl.h
@@ -0,0 +1,77 @@
+// Copyright 2017 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_SIGNIN_PROCESS_DICE_HEADER_DELEGATE_IMPL_H_
+#define CHROME_BROWSER_SIGNIN_PROCESS_DICE_HEADER_DELEGATE_IMPL_H_
+
+#include "base/memory/raw_ptr.h"
+#include "chrome/browser/signin/dice_response_handler.h"
+
+#include <memory>
+#include <string>
+
+#include "base/callback_forward.h"
+#include "base/memory/weak_ptr.h"
+#include "components/signin/public/base/signin_metrics.h"
+
+namespace content {
+class WebContents;
+}
+
+class Profile;
+class SigninUIError;
+
+class ProcessDiceHeaderDelegateImpl : public ProcessDiceHeaderDelegate {
+ public:
+ // Callback starting Sync.
+ using EnableSyncCallback =
+ base::OnceCallback<void(Profile*,
+ signin_metrics::AccessPoint,
+ signin_metrics::PromoAction,
+ signin_metrics::Reason,
+ content::WebContents*,
+ const CoreAccountId&)>;
+
+ // Callback showing a signin error UI.
+ using ShowSigninErrorCallback = base::OnceCallback<
+ void(Profile*, content::WebContents*, const SigninUIError&)>;
+
+ // |is_sync_signin_tab| is true if a sync signin flow has been started in that
+ // tab.
+ ProcessDiceHeaderDelegateImpl(
+ content::WebContents* web_contents,
+ EnableSyncCallback enable_sync_callback,
+ ShowSigninErrorCallback show_signin_error_callback);
+
+ ProcessDiceHeaderDelegateImpl(const ProcessDiceHeaderDelegateImpl&) = delete;
+ ProcessDiceHeaderDelegateImpl& operator=(
+ const ProcessDiceHeaderDelegateImpl&) = delete;
+
+ ~ProcessDiceHeaderDelegateImpl() override;
+
+ // ProcessDiceHeaderDelegate:
+ void HandleTokenExchangeSuccess(CoreAccountId account_id,
+ bool is_new_account) override;
+ void EnableSync(const CoreAccountId& account_id) override;
+ void HandleTokenExchangeFailure(const std::string& email,
+ const GoogleServiceAuthError& error) override;
+
+ private:
+ // Returns true if sync should be enabled after the user signs in.
+ bool ShouldEnableSync();
+
+ const base::WeakPtr<content::WebContents> web_contents_;
+ raw_ptr<Profile> profile_;
+ EnableSyncCallback enable_sync_callback_;
+ ShowSigninErrorCallback show_signin_error_callback_;
+ bool is_sync_signin_tab_ = false;
+ signin_metrics::AccessPoint access_point_ =
+ signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN;
+ signin_metrics::PromoAction promo_action_ =
+ signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO;
+ signin_metrics::Reason reason_ = signin_metrics::Reason::kUnknownReason;
+ GURL redirect_url_;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_PROCESS_DICE_HEADER_DELEGATE_IMPL_H_
diff --git a/chromium/chrome/browser/signin/process_dice_header_delegate_impl_unittest.cc b/chromium/chrome/browser/signin/process_dice_header_delegate_impl_unittest.cc
new file mode 100644
index 00000000000..60c842301f9
--- /dev/null
+++ b/chromium/chrome/browser/signin/process_dice_header_delegate_impl_unittest.cc
@@ -0,0 +1,375 @@
+// Copyright 2017 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.
+
+#include "chrome/browser/signin/process_dice_header_delegate_impl.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/strings/utf_string_conversions.h"
+#include "chrome/browser/signin/dice_tab_helper.h"
+#include "chrome/browser/signin/dice_web_signin_interceptor.h"
+#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
+#include "chrome/browser/ui/webui/signin/signin_ui_error.h"
+#include "chrome/common/url_constants.h"
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "components/signin/public/base/account_consistency_method.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/navigation_simulator.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using signin_metrics::Reason;
+
+namespace {
+
+signin_metrics::AccessPoint kTestAccessPoint =
+ signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE;
+
+signin_metrics::PromoAction kTestPromoAction =
+ signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO;
+
+// Dummy delegate that declines all interceptions.
+class TestDiceWebSigninInterceptorDelegate
+ : public DiceWebSigninInterceptor::Delegate {
+ public:
+ ~TestDiceWebSigninInterceptorDelegate() override = default;
+ std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle>
+ ShowSigninInterceptionBubble(
+ content::WebContents* web_contents,
+ const BubbleParameters& bubble_parameters,
+ base::OnceCallback<void(SigninInterceptionResult)> callback) override {
+ std::move(callback).Run(SigninInterceptionResult::kDeclined);
+ return nullptr;
+ }
+
+ void ShowProfileCustomizationBubble(Browser* browser) override {}
+};
+
+class MockDiceWebSigninInterceptor : public DiceWebSigninInterceptor {
+ public:
+ explicit MockDiceWebSigninInterceptor(Profile* profile)
+ : DiceWebSigninInterceptor(
+ profile,
+ std::make_unique<TestDiceWebSigninInterceptorDelegate>()) {}
+ ~MockDiceWebSigninInterceptor() override = default;
+
+ MOCK_METHOD(void,
+ MaybeInterceptWebSignin,
+ (content::WebContents * web_contents,
+ CoreAccountId account_id,
+ bool is_new_account,
+ bool is_sync_signin),
+ (override));
+};
+
+std::unique_ptr<KeyedService> CreateMockDiceWebSigninInterceptor(
+ content::BrowserContext* context) {
+ return std::make_unique<MockDiceWebSigninInterceptor>(
+ Profile::FromBrowserContext(context));
+}
+
+class ProcessDiceHeaderDelegateImplTest
+ : public ChromeRenderViewHostTestHarness {
+ public:
+ ProcessDiceHeaderDelegateImplTest()
+ : enable_sync_called_(false),
+ show_error_called_(false),
+ account_id_("12345"),
+ email_("foo@bar.com"),
+ auth_error_(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS) {}
+
+ ~ProcessDiceHeaderDelegateImplTest() override {}
+
+ void AddAccount(bool is_primary) {
+ if (!identity_test_environment_profile_adaptor_)
+ InitializeIdentityTestEnvironment();
+ if (is_primary) {
+ identity_test_environment_profile_adaptor_->identity_test_env()
+ ->SetPrimaryAccount(email_, signin::ConsentLevel::kSync);
+ } else {
+ identity_test_environment_profile_adaptor_->identity_test_env()
+ ->MakeAccountAvailable(email_);
+ }
+ }
+
+ void InitializeIdentityTestEnvironment() {
+ DCHECK(profile());
+ identity_test_environment_profile_adaptor_ =
+ std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile());
+ }
+
+ // Creates a ProcessDiceHeaderDelegateImpl instance.
+ std::unique_ptr<ProcessDiceHeaderDelegateImpl>
+ CreateDelegateAndNavigateToSignin(
+ bool is_sync_signin_tab,
+ Reason reason = Reason::kSigninPrimaryAccount) {
+ signin_reason_ = reason;
+ if (!identity_test_environment_profile_adaptor_)
+ InitializeIdentityTestEnvironment();
+ // Load the signin page.
+ std::unique_ptr<content::NavigationSimulator> simulator =
+ content::NavigationSimulator::CreateRendererInitiated(signin_url_,
+ main_rfh());
+ simulator->Start();
+ if (is_sync_signin_tab) {
+ DiceTabHelper::CreateForWebContents(web_contents());
+ DiceTabHelper* dice_tab_helper =
+ DiceTabHelper::FromWebContents(web_contents());
+ dice_tab_helper->InitializeSigninFlow(signin_url_, kTestAccessPoint,
+ signin_reason_, kTestPromoAction,
+ GURL::EmptyGURL());
+ }
+ simulator->Commit();
+ DCHECK_EQ(signin_url_, web_contents()->GetVisibleURL());
+ return std::make_unique<ProcessDiceHeaderDelegateImpl>(
+ web_contents(),
+ base::BindOnce(&ProcessDiceHeaderDelegateImplTest::StartSyncCallback,
+ base::Unretained(this)),
+ base::BindOnce(
+ &ProcessDiceHeaderDelegateImplTest::ShowSigninErrorCallback,
+ base::Unretained(this)));
+ }
+
+ // ChromeRenderViewHostTestHarness:
+ TestingProfile::TestingFactories GetTestingFactories() const override {
+ TestingProfile::TestingFactories factories = {
+ {DiceWebSigninInterceptorFactory::GetInstance(),
+ base::BindRepeating(&CreateMockDiceWebSigninInterceptor)}};
+ IdentityTestEnvironmentProfileAdaptor::
+ AppendIdentityTestEnvironmentFactories(&factories);
+ return factories;
+ }
+
+ void TearDown() override {
+ identity_test_environment_profile_adaptor_.reset();
+ ChromeRenderViewHostTestHarness::TearDown();
+ }
+
+ // Callback for the ProcessDiceHeaderDelegateImpl.
+ void StartSyncCallback(Profile* profile,
+ signin_metrics::AccessPoint access_point,
+ signin_metrics::PromoAction promo_action,
+ signin_metrics::Reason reason,
+ content::WebContents* contents,
+ const CoreAccountId& account_id) {
+ EXPECT_EQ(profile, this->profile());
+ EXPECT_EQ(access_point, kTestAccessPoint);
+ EXPECT_EQ(promo_action, kTestPromoAction);
+ EXPECT_EQ(reason, signin_reason_);
+ EXPECT_EQ(web_contents(), contents);
+ EXPECT_EQ(account_id_, account_id);
+ enable_sync_called_ = true;
+ }
+
+ // Callback for the ProcessDiceHeaderDelegateImpl.
+ void ShowSigninErrorCallback(Profile* profile,
+ content::WebContents* contents,
+ const SigninUIError& error) {
+ EXPECT_EQ(profile, this->profile());
+ EXPECT_EQ(web_contents(), contents);
+ EXPECT_EQ(base::UTF8ToUTF16(auth_error_.ToString()), error.message());
+ EXPECT_EQ(base::UTF8ToUTF16(email_), error.email());
+ show_error_called_ = true;
+ }
+
+ MockDiceWebSigninInterceptor* mock_interceptor() {
+ return static_cast<MockDiceWebSigninInterceptor*>(
+ DiceWebSigninInterceptorFactory::GetForProfile(profile()));
+ }
+
+ std::unique_ptr<IdentityTestEnvironmentProfileAdaptor>
+ identity_test_environment_profile_adaptor_;
+
+ const GURL signin_url_ = GURL("https://accounts.google.com");
+ bool enable_sync_called_;
+ bool show_error_called_;
+ CoreAccountId account_id_;
+ std::string email_;
+ GoogleServiceAuthError auth_error_;
+ Reason signin_reason_ = Reason::kSigninPrimaryAccount;
+};
+
+// Check that sync is enabled if the tab is closed during signin.
+TEST_F(ProcessDiceHeaderDelegateImplTest, CloseTabWhileStartingSync) {
+ std::unique_ptr<ProcessDiceHeaderDelegateImpl> delegate =
+ CreateDelegateAndNavigateToSignin(true);
+
+ // Close the tab.
+ DeleteContents();
+
+ // Check expectations.
+ delegate->EnableSync(account_id_);
+ EXPECT_TRUE(enable_sync_called_);
+ EXPECT_FALSE(show_error_called_);
+}
+
+// Check that the error is still shown if the tab is closed before the error is
+// received.
+TEST_F(ProcessDiceHeaderDelegateImplTest, CloseTabWhileFailingSignin) {
+ std::unique_ptr<ProcessDiceHeaderDelegateImpl> delegate =
+ CreateDelegateAndNavigateToSignin(true);
+
+ // Close the tab.
+ DeleteContents();
+
+ // Check expectations.
+ delegate->HandleTokenExchangeFailure(email_, auth_error_);
+ EXPECT_FALSE(enable_sync_called_);
+ EXPECT_TRUE(show_error_called_);
+}
+
+struct TestConfiguration {
+ // Test setup.
+ bool signed_in; // User was already signed in at the start of the flow.
+ bool signin_tab; // The tab is marked as a Sync signin tab.
+
+ // Test expectations.
+ bool callback_called; // The relevant callback was called.
+ bool show_ntp; // The NTP was shown.
+};
+
+TestConfiguration kEnableSyncTestCases[] = {
+ // clang-format off
+ // signed_in | signin_tab | callback_called | show_ntp
+ { false, false, false, false},
+ { false, true, true, true},
+ { true, false, false, false},
+ { true, true, false, false},
+ // clang-format on
+};
+
+// Parameterized version of ProcessDiceHeaderDelegateImplTest.
+class ProcessDiceHeaderDelegateImplTestEnableSync
+ : public ProcessDiceHeaderDelegateImplTest,
+ public ::testing::WithParamInterface<TestConfiguration> {};
+
+// Test the EnableSync() method in all configurations.
+TEST_P(ProcessDiceHeaderDelegateImplTestEnableSync, EnableSync) {
+ if (GetParam().signed_in)
+ AddAccount(/*is_primary=*/true);
+ std::unique_ptr<ProcessDiceHeaderDelegateImpl> delegate =
+ CreateDelegateAndNavigateToSignin(GetParam().signin_tab);
+ delegate->EnableSync(account_id_);
+ EXPECT_EQ(GetParam().callback_called, enable_sync_called_);
+ GURL expected_url =
+ GetParam().show_ntp ? GURL(chrome::kChromeUINewTabURL) : signin_url_;
+ EXPECT_EQ(expected_url, web_contents()->GetVisibleURL());
+ EXPECT_FALSE(show_error_called_);
+ // Check that the sync signin flow is complete.
+ if (GetParam().signin_tab) {
+ DiceTabHelper* dice_tab_helper =
+ DiceTabHelper::FromWebContents(web_contents());
+ ASSERT_TRUE(dice_tab_helper);
+ EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress());
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(All,
+ ProcessDiceHeaderDelegateImplTestEnableSync,
+ ::testing::ValuesIn(kEnableSyncTestCases));
+
+TestConfiguration kHandleTokenExchangeFailureTestCases[] = {
+ // clang-format off
+ // signed_in | signin_tab | callback_called | show_ntp
+ { false, false, true, false},
+ { false, true, true, true},
+ { true, false, true, false},
+ { true, true, true, false},
+ // clang-format on
+};
+
+// Parameterized version of ProcessDiceHeaderDelegateImplTest.
+class ProcessDiceHeaderDelegateImplTestHandleTokenExchangeFailure
+ : public ProcessDiceHeaderDelegateImplTest,
+ public ::testing::WithParamInterface<TestConfiguration> {};
+
+// Test the HandleTokenExchangeFailure() method in all configurations.
+TEST_P(ProcessDiceHeaderDelegateImplTestHandleTokenExchangeFailure,
+ HandleTokenExchangeFailure) {
+ if (GetParam().signed_in)
+ AddAccount(/*is_primary=*/true);
+ std::unique_ptr<ProcessDiceHeaderDelegateImpl> delegate =
+ CreateDelegateAndNavigateToSignin(GetParam().signin_tab);
+ delegate->HandleTokenExchangeFailure(email_, auth_error_);
+ EXPECT_FALSE(enable_sync_called_);
+ EXPECT_EQ(GetParam().callback_called, show_error_called_);
+ GURL expected_url =
+ GetParam().show_ntp ? GURL(chrome::kChromeUINewTabURL) : signin_url_;
+ EXPECT_EQ(expected_url, web_contents()->GetVisibleURL());
+ // Check that the sync signin flow is complete.
+ if (GetParam().signin_tab) {
+ DiceTabHelper* dice_tab_helper =
+ DiceTabHelper::FromWebContents(web_contents());
+ ASSERT_TRUE(dice_tab_helper);
+ EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress());
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ All,
+ ProcessDiceHeaderDelegateImplTestHandleTokenExchangeFailure,
+ ::testing::ValuesIn(kHandleTokenExchangeFailureTestCases));
+
+struct TokenExchangeSuccessConfiguration {
+ bool is_reauth; // User was already signed in with the account.
+ bool signin_tab; // A DiceTabHelper is attached to the tab.
+ Reason reason;
+ bool sync_signin; // Expected value for the MaybeInterceptWebSigin call.
+};
+
+TokenExchangeSuccessConfiguration kHandleTokenExchangeSuccessTestCases[] = {
+ // clang-format off
+ // is_reauth | signin_tab | reason    | sync_signin
+ { false, false, Reason::kSigninPrimaryAccount, false },
+ { false, true, Reason::kSigninPrimaryAccount, true },
+ { false, true, Reason::kAddSecondaryAccount, false },
+ { true, false, Reason::kSigninPrimaryAccount, false },
+ { true, true, Reason::kSigninPrimaryAccount, true },
+ // clang-format on
+};
+
+// Parameterized version of ProcessDiceHeaderDelegateImplTest.
+class ProcessDiceHeaderDelegateImplTestHandleTokenExchangeSuccess
+ : public ProcessDiceHeaderDelegateImplTest,
+ public ::testing::WithParamInterface<TokenExchangeSuccessConfiguration> {
+};
+
+// Test the HandleTokenExchangeSuccess() method in all configurations.
+TEST_P(ProcessDiceHeaderDelegateImplTestHandleTokenExchangeSuccess,
+ HandleTokenExchangeSuccess) {
+ if (GetParam().is_reauth)
+ AddAccount(/*is_primary=*/false);
+ std::unique_ptr<ProcessDiceHeaderDelegateImpl> delegate =
+ CreateDelegateAndNavigateToSignin(GetParam().signin_tab,
+ GetParam().reason);
+ EXPECT_CALL(
+ *mock_interceptor(),
+ MaybeInterceptWebSignin(web_contents(), account_id_,
+ !GetParam().is_reauth, GetParam().sync_signin));
+ delegate->HandleTokenExchangeSuccess(account_id_, !GetParam().is_reauth);
+
+ // Check that the sync signin flow is complete.
+ if (GetParam().signin_tab) {
+ DiceTabHelper* dice_tab_helper =
+ DiceTabHelper::FromWebContents(web_contents());
+ ASSERT_TRUE(dice_tab_helper);
+ EXPECT_EQ(GetParam().sync_signin,
+ dice_tab_helper->IsSyncSigninInProgress());
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ All,
+ ProcessDiceHeaderDelegateImplTestHandleTokenExchangeSuccess,
+ ::testing::ValuesIn(kHandleTokenExchangeSuccessTestCases));
+
+} // namespace
diff --git a/chromium/chrome/browser/signin/reauth_result.h b/chromium/chrome/browser/signin/reauth_result.h
new file mode 100644
index 00000000000..9aa85bed81c
--- /dev/null
+++ b/chromium/chrome/browser/signin/reauth_result.h
@@ -0,0 +1,38 @@
+// Copyright 2020 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_SIGNIN_REAUTH_RESULT_H_
+#define CHROME_BROWSER_SIGNIN_REAUTH_RESULT_H_
+
+namespace signin {
+
+// Indicates the result of the Gaia Reauth flow.
+// Needs to be kept in sync with "SigninReauthResult" in enums.xml.
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class ReauthResult {
+ // The user was successfully re-authenticated.
+ kSuccess = 0,
+
+ // The user account is not signed in.
+ kAccountNotSignedIn = 1,
+
+ // The user dismissed the reauth prompt.
+ kDismissedByUser = 2,
+
+ // The reauth page failed to load.
+ kLoadFailed = 3,
+
+ // A caller canceled the reauth flow.
+ kCancelled = 4,
+
+ // An unexpected response was received from Gaia.
+ kUnexpectedResponse = 5,
+
+ kMaxValue = kUnexpectedResponse,
+};
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_REAUTH_RESULT_H_
diff --git a/chromium/chrome/browser/signin/reauth_tab_helper.cc b/chromium/chrome/browser/signin/reauth_tab_helper.cc
new file mode 100644
index 00000000000..e8bd1d8b1c8
--- /dev/null
+++ b/chromium/chrome/browser/signin/reauth_tab_helper.cc
@@ -0,0 +1,96 @@
+// Copyright 2020 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.
+
+#include "chrome/browser/signin/reauth_tab_helper.h"
+#include "base/memory/ptr_util.h"
+#include "chrome/browser/signin/reauth_result.h"
+#include "content/public/browser/navigation_handle.h"
+#include "net/http/http_status_code.h"
+#include "url/origin.h"
+
+namespace signin {
+
+namespace {
+
+bool IsExpectedResponseCode(int response_code) {
+ return response_code == net::HTTP_OK || response_code == net::HTTP_NO_CONTENT;
+}
+
+} // namespace
+
+// static
+void ReauthTabHelper::CreateForWebContents(content::WebContents* web_contents,
+ const GURL& reauth_url,
+ ReauthCallback callback) {
+ DCHECK(web_contents);
+ if (!FromWebContents(web_contents)) {
+ web_contents->SetUserData(
+ UserDataKey(), base::WrapUnique(new ReauthTabHelper(
+ web_contents, reauth_url, std::move(callback))));
+ } else {
+ std::move(callback).Run(signin::ReauthResult::kCancelled);
+ }
+}
+
+ReauthTabHelper::~ReauthTabHelper() = default;
+
+void ReauthTabHelper::CompleteReauth(signin::ReauthResult result) {
+ if (callback_)
+ std::move(callback_).Run(result);
+}
+
+void ReauthTabHelper::DidFinishNavigation(
+ content::NavigationHandle* navigation_handle) {
+ if (!navigation_handle->IsInPrimaryMainFrame())
+ return;
+
+ is_within_reauth_origin_ &=
+ url::IsSameOriginWith(reauth_url_, navigation_handle->GetURL());
+
+ if (navigation_handle->IsErrorPage()) {
+ has_last_committed_error_page_ = true;
+ return;
+ }
+
+ has_last_committed_error_page_ = false;
+
+ GURL::Replacements replacements;
+ replacements.ClearQuery();
+ GURL url_without_query =
+ navigation_handle->GetURL().ReplaceComponents(replacements);
+ if (url_without_query != reauth_url_)
+ return;
+
+ if (!navigation_handle->GetResponseHeaders() ||
+ !IsExpectedResponseCode(
+ navigation_handle->GetResponseHeaders()->response_code())) {
+ CompleteReauth(signin::ReauthResult::kUnexpectedResponse);
+ }
+
+ CompleteReauth(signin::ReauthResult::kSuccess);
+}
+
+void ReauthTabHelper::WebContentsDestroyed() {
+ CompleteReauth(signin::ReauthResult::kDismissedByUser);
+}
+
+bool ReauthTabHelper::is_within_reauth_origin() {
+ return is_within_reauth_origin_;
+}
+
+bool ReauthTabHelper::has_last_committed_error_page() {
+ return has_last_committed_error_page_;
+}
+
+ReauthTabHelper::ReauthTabHelper(content::WebContents* web_contents,
+ const GURL& reauth_url,
+ ReauthCallback callback)
+ : content::WebContentsUserData<ReauthTabHelper>(*web_contents),
+ content::WebContentsObserver(web_contents),
+ reauth_url_(reauth_url),
+ callback_(std::move(callback)) {}
+
+WEB_CONTENTS_USER_DATA_KEY_IMPL(ReauthTabHelper);
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/reauth_tab_helper.h b/chromium/chrome/browser/signin/reauth_tab_helper.h
new file mode 100644
index 00000000000..f830c24caec
--- /dev/null
+++ b/chromium/chrome/browser/signin/reauth_tab_helper.h
@@ -0,0 +1,66 @@
+// Copyright 2020 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_SIGNIN_REAUTH_TAB_HELPER_H_
+#define CHROME_BROWSER_SIGNIN_REAUTH_TAB_HELPER_H_
+
+#include "base/callback.h"
+#include "content/public/browser/web_contents_observer.h"
+#include "content/public/browser/web_contents_user_data.h"
+#include "url/gurl.h"
+
+namespace signin {
+
+enum class ReauthResult;
+
+// Tab helper class observing navigations within the reauth flow and notifying
+// a caller about a flow result.
+class ReauthTabHelper : public content::WebContentsUserData<ReauthTabHelper>,
+ public content::WebContentsObserver {
+ public:
+ using ReauthCallback = base::OnceCallback<void(signin::ReauthResult)>;
+
+ // Creates a new ReauthTabHelper and attaches it to |web_contents|. If an
+ // instance is already attached, no replacement happens, just notifies the
+ // caller by invoking |callback| with signin::ReauthResult::kCancelled.
+ // Initializes a helper with:
+ // - |callback| to be called when the reauth flow is complete.
+ // - |reauth_url| that should be the final destination of the reauth flow.
+ static void CreateForWebContents(content::WebContents* web_contents,
+ const GURL& reauth_url,
+ ReauthCallback callback);
+
+ ReauthTabHelper(const ReauthTabHelper&) = delete;
+ ReauthTabHelper& operator=(const ReauthTabHelper&) = delete;
+
+ ~ReauthTabHelper() override;
+
+ // If |callback_| is not null, calls |callback_| with |result|.
+ void CompleteReauth(signin::ReauthResult result);
+
+ // content::WebContentsObserver
+ void DidFinishNavigation(
+ content::NavigationHandle* navigation_handle) override;
+ void WebContentsDestroyed() override;
+
+ bool is_within_reauth_origin();
+ bool has_last_committed_error_page();
+
+ private:
+ friend class content::WebContentsUserData<ReauthTabHelper>;
+ explicit ReauthTabHelper(content::WebContents* web_contents,
+ const GURL& reauth_url,
+ ReauthCallback callback);
+
+ const GURL reauth_url_;
+ ReauthCallback callback_;
+ bool is_within_reauth_origin_ = true;
+ bool has_last_committed_error_page_ = false;
+
+ WEB_CONTENTS_USER_DATA_KEY_DECL();
+};
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_REAUTH_TAB_HELPER_H_
diff --git a/chromium/chrome/browser/signin/reauth_tab_helper_unittest.cc b/chromium/chrome/browser/signin/reauth_tab_helper_unittest.cc
new file mode 100644
index 00000000000..a17078cb067
--- /dev/null
+++ b/chromium/chrome/browser/signin/reauth_tab_helper_unittest.cc
@@ -0,0 +1,184 @@
+// Copyright 2020 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.
+
+#include "chrome/browser/signin/reauth_tab_helper.h"
+
+#include "base/memory/raw_ptr.h"
+#include "base/test/mock_callback.h"
+#include "chrome/browser/signin/reauth_result.h"
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "content/public/test/navigation_simulator.h"
+#include "content/public/test/web_contents_tester.h"
+#include "net/base/net_errors.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/features.h"
+
+namespace signin {
+
+class ReauthTabHelperTest : public ChromeRenderViewHostTestHarness {
+ public:
+ ReauthTabHelperTest()
+ : reauth_url_("https://my-identity_provider.com/reauth") {}
+
+ void SetUp() override {
+ ChromeRenderViewHostTestHarness::SetUp();
+
+ ReauthTabHelper::CreateForWebContents(web_contents(), reauth_url(),
+ mock_callback_.Get());
+ tab_helper_ = ReauthTabHelper::FromWebContents(web_contents());
+ }
+
+ ReauthTabHelper* tab_helper() { return tab_helper_; }
+
+ base::MockOnceCallback<void(signin::ReauthResult)>* mock_callback() {
+ return &mock_callback_;
+ }
+
+ const GURL& reauth_url() { return reauth_url_; }
+
+ private:
+ raw_ptr<ReauthTabHelper> tab_helper_ = nullptr;
+ base::MockOnceCallback<void(signin::ReauthResult)> mock_callback_;
+ const GURL reauth_url_;
+};
+
+// Tests a direct call to CompleteReauth().
+TEST_F(ReauthTabHelperTest, CompleteReauth) {
+ signin::ReauthResult result = signin::ReauthResult::kSuccess;
+ EXPECT_CALL(*mock_callback(), Run(result));
+ tab_helper()->CompleteReauth(result);
+}
+
+// Tests a successful navigation to the reauth URL.
+TEST_F(ReauthTabHelperTest, NavigateToReauthURL) {
+ auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
+ reauth_url(), web_contents());
+ simulator->Start();
+ EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess));
+ simulator->Commit();
+}
+
+// Tests the reauth flow when the reauth URL has query parameters.
+TEST_F(ReauthTabHelperTest, NavigateToReauthURLWithQuery) {
+ auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
+ reauth_url().Resolve("?rapt=35be36ae"), web_contents());
+ simulator->Start();
+ EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess));
+ simulator->Commit();
+}
+
+// Tests the reauth flow with multiple navigations within the same origin.
+TEST_F(ReauthTabHelperTest, MultipleNavigationReauth) {
+ auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
+ reauth_url(), web_contents());
+ simulator->Start();
+ simulator->Redirect(
+ reauth_url().DeprecatedGetOriginAsURL().Resolve("/login"));
+ simulator->Commit();
+
+ auto simulator2 = content::NavigationSimulator::CreateRendererInitiated(
+ reauth_url(), main_rfh());
+ simulator2->Start();
+ EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess));
+ simulator2->Commit();
+}
+
+// Tests the reauth flow with multiple navigations across two different origins.
+// TODO(https://crbug.com/1045515): update this test once navigations outside of
+// reauth_url() are blocked.
+TEST_F(ReauthTabHelperTest, MultipleNavigationReauthThroughExternalOrigin) {
+ auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
+ reauth_url(), web_contents());
+ simulator->Start();
+ simulator->Redirect(GURL("https://other-identity-provider.com/login"));
+ simulator->Commit();
+
+ auto simulator2 = content::NavigationSimulator::CreateRendererInitiated(
+ reauth_url(), main_rfh());
+ simulator2->Start();
+ EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess));
+ simulator2->Commit();
+}
+
+// Tests a failed navigation to the reauth URL, followed by a successful
+// navigation.
+TEST_F(ReauthTabHelperTest, NavigationToReauthURLFailed) {
+ auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
+ reauth_url(), web_contents());
+ simulator->Start();
+ simulator->Fail(net::ERR_TIMED_OUT);
+ simulator->CommitErrorPage();
+ EXPECT_TRUE(tab_helper()->has_last_committed_error_page());
+ // Check that the navigation still counts as within the same origin.
+ EXPECT_TRUE(tab_helper()->is_within_reauth_origin());
+
+ auto simulator2 = content::NavigationSimulator::CreateRendererInitiated(
+ reauth_url(), main_rfh());
+ simulator2->Start();
+ EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess));
+ simulator2->Commit();
+ EXPECT_FALSE(tab_helper()->has_last_committed_error_page());
+}
+
+// Tests a failed navigation redirecting to an external origin, followed by a
+// successful navigation.
+TEST_F(ReauthTabHelperTest, NavigationToExternalOriginFailed) {
+ auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
+ reauth_url(), web_contents());
+ simulator->Start();
+ simulator->Redirect(GURL("https://other-identity-provider.com/login"));
+ simulator->Fail(net::ERR_TIMED_OUT);
+ simulator->CommitErrorPage();
+ EXPECT_TRUE(tab_helper()->has_last_committed_error_page());
+ // Check that the navigation doesn't count as within the same origin.
+ EXPECT_FALSE(tab_helper()->is_within_reauth_origin());
+
+ auto simulator2 = content::NavigationSimulator::CreateRendererInitiated(
+ reauth_url(), main_rfh());
+ simulator2->Start();
+ EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess));
+ simulator2->Commit();
+ EXPECT_FALSE(tab_helper()->has_last_committed_error_page());
+ EXPECT_FALSE(tab_helper()->is_within_reauth_origin());
+}
+
+// Tests the WebContents deletion.
+TEST_F(ReauthTabHelperTest, WebContentsDestroyed) {
+ EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kDismissedByUser));
+ DeleteContents();
+}
+
+class ReauthTabHelperPrerenderTest : public ReauthTabHelperTest {
+ public:
+ ReauthTabHelperPrerenderTest() {
+ feature_list_.InitWithFeatures(
+ {blink::features::kPrerender2},
+ // Disable the memory requirement of Prerender2 so the test can run on
+ // any bot.
+ {blink::features::kPrerender2MemoryControls});
+ }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+};
+
+TEST_F(ReauthTabHelperPrerenderTest,
+ PrerenderDoesNotAffectLastCommittedErrorPage) {
+ content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
+ reauth_url());
+ EXPECT_FALSE(tab_helper()->has_last_committed_error_page());
+
+ // Fail prerendering navigation.
+ const GURL prerender_url = reauth_url().Resolve("?prerendering");
+ auto simulator = content::WebContentsTester::For(web_contents())
+ ->AddPrerenderAndStartNavigation(prerender_url);
+ simulator->Fail(net::ERR_TIMED_OUT);
+ simulator->CommitErrorPage();
+
+ // has_last_committed_error_page_ is not updated by preredering.
+ EXPECT_FALSE(tab_helper()->has_last_committed_error_page());
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/reauth_util.cc b/chromium/chrome/browser/signin/reauth_util.cc
new file mode 100644
index 00000000000..b770da50fb0
--- /dev/null
+++ b/chromium/chrome/browser/signin/reauth_util.cc
@@ -0,0 +1,40 @@
+// Copyright 2020 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.
+
+#include <string>
+
+#include "base/strings/string_number_conversions.h"
+#include "chrome/browser/signin/reauth_util.h"
+#include "chrome/common/webui_url_constants.h"
+#include "net/base/url_util.h"
+
+namespace signin {
+
+GURL GetReauthConfirmationURL(signin_metrics::ReauthAccessPoint access_point) {
+ GURL url = GURL(chrome::kChromeUISigninReauthURL);
+ url = net::AppendQueryParameter(
+ url, "access_point",
+ base::NumberToString(static_cast<int>(access_point)));
+ return url;
+}
+
+signin_metrics::ReauthAccessPoint GetReauthAccessPointForReauthConfirmationURL(
+ const GURL& url) {
+ std::string value;
+ if (!net::GetValueForKeyInQuery(url, "access_point", &value))
+ return signin_metrics::ReauthAccessPoint::kUnknown;
+
+ int access_point = -1;
+ base::StringToInt(value, &access_point);
+ if (access_point <=
+ static_cast<int>(signin_metrics::ReauthAccessPoint::kUnknown) ||
+ access_point >
+ static_cast<int>(signin_metrics::ReauthAccessPoint::kMaxValue)) {
+ return signin_metrics::ReauthAccessPoint::kUnknown;
+ }
+
+ return static_cast<signin_metrics::ReauthAccessPoint>(access_point);
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/reauth_util.h b/chromium/chrome/browser/signin/reauth_util.h
new file mode 100644
index 00000000000..52150ee53f1
--- /dev/null
+++ b/chromium/chrome/browser/signin/reauth_util.h
@@ -0,0 +1,24 @@
+// Copyright 2020 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_SIGNIN_REAUTH_UTIL_H_
+#define CHROME_BROWSER_SIGNIN_REAUTH_UTIL_H_
+
+#include "components/signin/public/base/signin_metrics.h"
+#include "url/gurl.h"
+
+namespace signin {
+
+// Returns a URL to display in the reauth confirmation dialog. The dialog was
+// triggered by |access_point|.
+GURL GetReauthConfirmationURL(signin_metrics::ReauthAccessPoint access_point);
+
+// Returns ReauthAccessPoint encoded in the query of the reauth confirmation
+// URL.
+signin_metrics::ReauthAccessPoint GetReauthAccessPointForReauthConfirmationURL(
+ const GURL& url);
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_REAUTH_UTIL_H_
diff --git a/chromium/chrome/browser/signin/reauth_util_unittest.cc b/chromium/chrome/browser/signin/reauth_util_unittest.cc
new file mode 100644
index 00000000000..44fb952465d
--- /dev/null
+++ b/chromium/chrome/browser/signin/reauth_util_unittest.cc
@@ -0,0 +1,33 @@
+// Copyright 2020 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.
+
+#include "chrome/browser/signin/reauth_util.h"
+
+#include "chrome/common/webui_url_constants.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace signin {
+
+class ReauthUtilURLTest : public ::testing::TestWithParam<int> {};
+
+TEST_P(ReauthUtilURLTest, GetAndParseReauthConfirmationURL) {
+ auto access_point =
+ static_cast<signin_metrics::ReauthAccessPoint>(GetParam());
+ GURL url = GetReauthConfirmationURL(access_point);
+ ASSERT_TRUE(url.is_valid());
+ EXPECT_EQ(url.host(), chrome::kChromeUISigninReauthHost);
+ signin_metrics::ReauthAccessPoint get_access_point =
+ GetReauthAccessPointForReauthConfirmationURL(url);
+ EXPECT_EQ(get_access_point, access_point);
+}
+
+INSTANTIATE_TEST_CASE_P(
+ AllAccessPoints,
+ ReauthUtilURLTest,
+ ::testing::Range(
+ static_cast<int>(signin_metrics::ReauthAccessPoint::kUnknown),
+ static_cast<int>(signin_metrics::ReauthAccessPoint::kMaxValue) + 1));
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/remove_local_account_browsertest.cc b/chromium/chrome/browser/signin/remove_local_account_browsertest.cc
new file mode 100644
index 00000000000..259f3d2857e
--- /dev/null
+++ b/chromium/chrome/browser/signin/remove_local_account_browsertest.cc
@@ -0,0 +1,143 @@
+// Copyright 2021 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.
+
+#include <memory>
+
+#include "base/run_loop.h"
+#include "base/test/bind.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_tabstrip.h"
+#include "chrome/test/base/mixin_based_in_process_browser_test.h"
+#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "components/signin/public/identity_manager/test_identity_manager_observer.h"
+#include "content/public/test/browser_test.h"
+#include "google_apis/gaia/fake_gaia.h"
+#include "google_apis/gaia/gaia_switches.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/test/embedded_test_server/http_response.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/ash/login/test/network_portal_detector_mixin.h"
+#endif
+
+namespace {
+
+using testing::Contains;
+using testing::Not;
+
+MATCHER_P(ListedAccountMatchesGaiaId, gaia_id, "") {
+ return arg.gaia_id == std::string(gaia_id);
+}
+
+const char kTestGaiaId[] = "123";
+
+class RemoveLocalAccountTest : public MixinBasedInProcessBrowserTest {
+ protected:
+ RemoveLocalAccountTest()
+ : embedded_test_server_(net::EmbeddedTestServer::TYPE_HTTPS) {
+ embedded_test_server_.RegisterRequestHandler(base::BindRepeating(
+ &FakeGaia::HandleRequest, base::Unretained(&fake_gaia_)));
+ }
+
+ ~RemoveLocalAccountTest() override = default;
+
+ signin::IdentityManager* identity_manager() {
+ return IdentityManagerFactory::GetForProfile(browser()->profile());
+ }
+
+ signin::AccountsInCookieJarInfo WaitUntilAccountsInCookieUpdated() {
+ signin::TestIdentityManagerObserver observer(identity_manager());
+ base::RunLoop run_loop;
+ observer.SetOnAccountsInCookieUpdatedCallback(run_loop.QuitClosure());
+ run_loop.Run();
+ return observer.AccountsInfoFromAccountsInCookieUpdatedCallback();
+ }
+
+ // MixinBasedInProcessBrowserTest:
+ void SetUpCommandLine(base::CommandLine* command_line) override {
+ MixinBasedInProcessBrowserTest::SetUpCommandLine(command_line);
+ ASSERT_TRUE(embedded_test_server_.InitializeAndListen());
+ const GURL base_url = embedded_test_server_.base_url();
+ command_line->AppendSwitchASCII(switches::kGaiaUrl, base_url.spec());
+ }
+
+ void SetUpOnMainThread() override {
+ MixinBasedInProcessBrowserTest::SetUpOnMainThread();
+ fake_gaia_.Initialize();
+
+ FakeGaia::MergeSessionParams params;
+ params.signed_out_gaia_ids.push_back(kTestGaiaId);
+ fake_gaia_.UpdateMergeSessionParams(params);
+
+ embedded_test_server_.StartAcceptingConnections();
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ // ChromeSigninClient uses chromeos::DelayNetworkCall() which requires
+ // simulating being online.
+ network_portal_detector_.SimulateDefaultNetworkState(
+ ash::NetworkPortalDetector::CAPTIVE_PORTAL_STATUS_ONLINE);
+#endif
+ }
+
+ FakeGaia fake_gaia_;
+ net::EmbeddedTestServer embedded_test_server_;
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ ash::NetworkPortalDetectorMixin network_portal_detector_{&mixin_host_};
+#endif
+};
+
+IN_PROC_BROWSER_TEST_F(RemoveLocalAccountTest, ShouldNotifyObservers) {
+ // To enforce an initial ListAccounts fetch and the corresponding notification
+ // to observers, make the current list as stale. This is done for the purpose
+ // of documenting assertions on the AccountsInCookieJarInfo passed to
+ // observers during notification.
+ signin::SetFreshnessOfAccountsInGaiaCookie(identity_manager(),
+ /*accounts_are_fresh=*/false);
+
+ ASSERT_FALSE(identity_manager()->GetAccountsInCookieJar().accounts_are_fresh);
+ const signin::AccountsInCookieJarInfo
+ cookie_jar_info_in_initial_notification =
+ WaitUntilAccountsInCookieUpdated();
+ ASSERT_TRUE(cookie_jar_info_in_initial_notification.accounts_are_fresh);
+ ASSERT_THAT(cookie_jar_info_in_initial_notification.signed_out_accounts,
+ Contains(ListedAccountMatchesGaiaId(kTestGaiaId)));
+
+ const signin::AccountsInCookieJarInfo initial_cookie_jar_info =
+ identity_manager()->GetAccountsInCookieJar();
+ ASSERT_TRUE(initial_cookie_jar_info.accounts_are_fresh);
+ ASSERT_THAT(initial_cookie_jar_info.signed_out_accounts,
+ Contains(ListedAccountMatchesGaiaId(kTestGaiaId)));
+
+ // Open a FakeGaia page that issues the desired HTTP response header with
+ // Google-Accounts-RemoveLocalAccount.
+ chrome::AddTabAt(browser(),
+ fake_gaia_.GetDummyRemoveLocalAccountURL(kTestGaiaId),
+ /*index=*/0,
+ /*foreground=*/true);
+
+ // Wait until observers are notified with OnAccountsInCookieUpdated().
+ const signin::AccountsInCookieJarInfo
+ cookie_jar_info_in_updated_notification =
+ WaitUntilAccountsInCookieUpdated();
+
+ EXPECT_TRUE(cookie_jar_info_in_updated_notification.accounts_are_fresh);
+ EXPECT_THAT(cookie_jar_info_in_updated_notification.signed_out_accounts,
+ Not(Contains(ListedAccountMatchesGaiaId(kTestGaiaId))));
+
+ const signin::AccountsInCookieJarInfo updated_cookie_jar_info =
+ identity_manager()->GetAccountsInCookieJar();
+ EXPECT_TRUE(updated_cookie_jar_info.accounts_are_fresh);
+ EXPECT_THAT(updated_cookie_jar_info.signed_out_accounts,
+ Not(Contains(ListedAccountMatchesGaiaId(kTestGaiaId))));
+}
+
+} // namespace
diff --git a/chromium/chrome/browser/signin/services/DIR_METADATA b/chromium/chrome/browser/signin/services/DIR_METADATA
new file mode 100644
index 00000000000..7a2580a646c
--- /dev/null
+++ b/chromium/chrome/browser/signin/services/DIR_METADATA
@@ -0,0 +1 @@
+os: ANDROID
diff --git a/chromium/chrome/browser/signin/services/OWNERS b/chromium/chrome/browser/signin/services/OWNERS
new file mode 100644
index 00000000000..1c49383244f
--- /dev/null
+++ b/chromium/chrome/browser/signin/services/OWNERS
@@ -0,0 +1,2 @@
+bsazonov@chromium.org
+aliceywang@chromium.org
diff --git a/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/ProfileDataCacheUnitTest.java b/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/ProfileDataCacheUnitTest.java
new file mode 100644
index 00000000000..f64eb974942
--- /dev/null
+++ b/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/ProfileDataCacheUnitTest.java
@@ -0,0 +1,120 @@
+// Copyright 2021 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.
+
+package org.chromium.chrome.browser.signin.services;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.mockito.quality.Strictness;
+import org.robolectric.RuntimeEnvironment;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.base.test.util.JniMocker;
+import org.chromium.chrome.R;
+import org.chromium.chrome.test.util.browser.signin.AccountManagerTestRule;
+import org.chromium.components.signin.base.AccountInfo;
+import org.chromium.components.signin.base.CoreAccountId;
+import org.chromium.components.signin.identitymanager.AccountInfoServiceProvider;
+import org.chromium.components.signin.identitymanager.AccountTrackerService;
+import org.chromium.components.signin.identitymanager.IdentityManager;
+import org.chromium.components.signin.identitymanager.IdentityManagerJni;
+
+/**
+ * Unit tests for {@link ProfileDataCache}
+ */
+@RunWith(BaseRobolectricTestRunner.class)
+public class ProfileDataCacheUnitTest {
+ private static final long NATIVE_IDENTITY_MANAGER = 10001L;
+ private static final String ACCOUNT_EMAIL = "test@gmail.com";
+
+ @Rule
+ public final MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
+
+ @Rule
+ public final JniMocker mocker = new JniMocker();
+
+ @Rule
+ public final AccountManagerTestRule mAccountManagerTestRule = new AccountManagerTestRule();
+
+ @Mock
+ private AccountTrackerService mAccountTrackerServiceMock;
+
+ @Mock
+ private IdentityManager.Natives mIdentityManagerNativeMock;
+
+ @Mock
+ private ProfileDataCache.Observer mObserverMock;
+
+ private final IdentityManager mIdentityManager =
+ IdentityManager.create(NATIVE_IDENTITY_MANAGER, null /* OAuth2TokenService */);
+
+ private ProfileDataCache mProfileDataCache;
+
+ @Before
+ public void setUp() {
+ mocker.mock(IdentityManagerJni.TEST_HOOKS, mIdentityManagerNativeMock);
+ mProfileDataCache = ProfileDataCache.createWithDefaultImageSizeAndNoBadge(
+ RuntimeEnvironment.application.getApplicationContext());
+
+ // Add an observer for IdentityManager::onExtendedAccountInfoUpdated.
+ mAccountManagerTestRule.observeIdentityManager(mIdentityManager);
+ }
+
+ @After
+ public void tearDown() {
+ AccountInfoServiceProvider.resetForTests();
+ }
+
+ @Test
+ public void accountInfoIsUpdatedWithOnlyFullName() {
+ final String fullName = "full name1";
+ final AccountInfo accountInfo = new AccountInfo(new CoreAccountId("gaia-id-test"),
+ ACCOUNT_EMAIL, "gaia-id-test", fullName, null, null);
+ mProfileDataCache.addObserver(mObserverMock);
+ Assert.assertFalse(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL));
+ Assert.assertNull(mProfileDataCache.getProfileDataOrDefault(ACCOUNT_EMAIL).getFullName());
+
+ mIdentityManager.onExtendedAccountInfoUpdated(accountInfo);
+
+ Assert.assertTrue(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL));
+ Assert.assertEquals(
+ fullName, mProfileDataCache.getProfileDataOrDefault(ACCOUNT_EMAIL).getFullName());
+ }
+
+ @Test
+ public void accountInfoIsUpdatedWithOnlyGivenName() {
+ final String givenName = "given name1";
+ final AccountInfo accountInfo = new AccountInfo(new CoreAccountId("gaia-id-test"),
+ ACCOUNT_EMAIL, "gaia-id-test", null, givenName, null);
+ mProfileDataCache.addObserver(mObserverMock);
+ Assert.assertFalse(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL));
+ Assert.assertNull(mProfileDataCache.getProfileDataOrDefault(ACCOUNT_EMAIL).getGivenName());
+
+ mIdentityManager.onExtendedAccountInfoUpdated(accountInfo);
+
+ Assert.assertTrue(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL));
+ Assert.assertEquals(
+ givenName, mProfileDataCache.getProfileDataOrDefault(ACCOUNT_EMAIL).getGivenName());
+ }
+
+ @Test
+ public void accountInfoIsUpdatedWithOnlyBadgeConfig() {
+ mProfileDataCache.setBadge(R.drawable.ic_sync_badge_error_20dp);
+ final AccountInfo accountInfo = new AccountInfo(
+ new CoreAccountId("gaia-id-test"), ACCOUNT_EMAIL, "gaia-id-test", null, null, null);
+ mProfileDataCache.addObserver(mObserverMock);
+ Assert.assertFalse(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL));
+
+ mIdentityManager.onExtendedAccountInfoUpdated(accountInfo);
+
+ Assert.assertTrue(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL));
+ }
+}
diff --git a/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/WebSigninBridgeTest.java b/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/WebSigninBridgeTest.java
new file mode 100644
index 00000000000..8acec65e1e9
--- /dev/null
+++ b/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/WebSigninBridgeTest.java
@@ -0,0 +1,89 @@
+// Copyright 2020 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.
+
+package org.chromium.chrome.browser.signin.services;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.base.test.util.JniMocker;
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.components.signin.base.CoreAccountInfo;
+import org.chromium.components.signin.base.GoogleServiceAuthError;
+import org.chromium.components.signin.base.GoogleServiceAuthError.State;
+
+/**
+ * Unit tests for {@link WebSigninBridge}.
+ */
+@RunWith(BaseRobolectricTestRunner.class)
+public class WebSigninBridgeTest {
+ private static final CoreAccountInfo CORE_ACCOUNT_INFO =
+ CoreAccountInfo.createFromEmailAndGaiaId("user@domain.com", "gaia-id-user");
+ private static final long NATIVE_WEB_SIGNIN_BRIDGE = 1000L;
+
+ @Rule
+ public final JniMocker mocker = new JniMocker();
+
+ @Rule
+ public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ @Mock
+ private WebSigninBridge.Natives mNativeMock;
+
+ @Mock
+ private Profile mProfileMock;
+
+ @Mock
+ private WebSigninBridge.Listener mListenerMock;
+
+ private final WebSigninBridge.Factory mFactory = new WebSigninBridge.Factory();
+
+ @Before
+ public void setUp() {
+ mocker.mock(WebSigninBridgeJni.TEST_HOOKS, mNativeMock);
+ when(mNativeMock.create(mProfileMock, CORE_ACCOUNT_INFO, mListenerMock))
+ .thenReturn(NATIVE_WEB_SIGNIN_BRIDGE);
+ }
+
+ @Test
+ public void testFactoryCreate() {
+ WebSigninBridge webSigninBridge =
+ mFactory.create(mProfileMock, CORE_ACCOUNT_INFO, mListenerMock);
+ Assert.assertNotNull("Factory#create should not return null!", webSigninBridge);
+ verify(mNativeMock).create(mProfileMock, CORE_ACCOUNT_INFO, mListenerMock);
+ }
+
+ @Test
+ public void testDestroy() {
+ mFactory.create(mProfileMock, CORE_ACCOUNT_INFO, mListenerMock).destroy();
+ verify(mNativeMock).destroy(NATIVE_WEB_SIGNIN_BRIDGE);
+ }
+
+ @Test
+ public void testOnSigninSucceed() {
+ WebSigninBridge.onSigninSucceeded(mListenerMock);
+ verify(mListenerMock).onSigninSucceeded();
+ verify(mListenerMock, never()).onSigninFailed(any());
+ }
+
+ @Test
+ public void testOnSigninFailed() {
+ final GoogleServiceAuthError error = new GoogleServiceAuthError(State.CONNECTION_FAILED);
+ WebSigninBridge.onSigninFailed(mListenerMock, error);
+ verify(mListenerMock).onSigninFailed(error);
+ verify(mListenerMock, never()).onSigninSucceeded();
+ }
+}
diff --git a/chromium/chrome/browser/signin/signin_error_controller_factory.cc b/chromium/chrome/browser/signin/signin_error_controller_factory.cc
new file mode 100644
index 00000000000..ebbd0612ba9
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_error_controller_factory.cc
@@ -0,0 +1,48 @@
+// Copyright 2015 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.
+
+#include "chrome/browser/signin/signin_error_controller_factory.h"
+
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+
+SigninErrorControllerFactory::SigninErrorControllerFactory()
+ : BrowserContextKeyedServiceFactory(
+ "SigninErrorController",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(IdentityManagerFactory::GetInstance());
+}
+
+SigninErrorControllerFactory::~SigninErrorControllerFactory() {}
+
+// static
+SigninErrorController* SigninErrorControllerFactory::GetForProfile(
+ Profile* profile) {
+ return static_cast<SigninErrorController*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+SigninErrorControllerFactory* SigninErrorControllerFactory::GetInstance() {
+ return base::Singleton<SigninErrorControllerFactory>::get();
+}
+
+KeyedService* SigninErrorControllerFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ SigninErrorController::AccountMode account_mode =
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ SigninErrorController::AccountMode::ANY_ACCOUNT;
+#else
+ AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile)
+ ? SigninErrorController::AccountMode::ANY_ACCOUNT
+ : SigninErrorController::AccountMode::PRIMARY_ACCOUNT;
+#endif
+ return new SigninErrorController(
+ account_mode, IdentityManagerFactory::GetForProfile(profile));
+}
diff --git a/chromium/chrome/browser/signin/signin_error_controller_factory.h b/chromium/chrome/browser/signin/signin_error_controller_factory.h
new file mode 100644
index 00000000000..0d87f49799e
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_error_controller_factory.h
@@ -0,0 +1,37 @@
+// Copyright 2015 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_SIGNIN_SIGNIN_ERROR_CONTROLLER_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_ERROR_CONTROLLER_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+#include "components/signin/core/browser/signin_error_controller.h"
+
+class Profile;
+
+// Singleton that owns all SigninErrorControllers and associates them with
+// Profiles.
+class SigninErrorControllerFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ // Returns the instance of SigninErrorController associated with this profile
+ // (creating one if none exists). Returns NULL if this profile cannot have an
+ // SigninClient (for example, if |profile| is incognito).
+ static SigninErrorController* GetForProfile(Profile* profile);
+
+ // Returns an instance of the factory singleton.
+ static SigninErrorControllerFactory* GetInstance();
+
+ private:
+ friend struct base::DefaultSingletonTraits<SigninErrorControllerFactory>;
+
+ SigninErrorControllerFactory();
+ ~SigninErrorControllerFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_ERROR_CONTROLLER_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/signin_features.cc b/chromium/chrome/browser/signin/signin_features.cc
new file mode 100644
index 00000000000..b1932809890
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_features.cc
@@ -0,0 +1,16 @@
+// Copyright 2020 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.
+
+#include "chrome/browser/signin/signin_features.h"
+
+// Enables the client-side processing of the HTTP response header
+// Google-Accounts-RemoveLocalAccount.
+const base::Feature kProcessGaiaRemoveLocalAccountHeader{
+ "ProcessGaiaRemoveLocalAccountHeader", base::FEATURE_ENABLED_BY_DEFAULT};
+
+// Allows policies to be loaded on a managed account without activating sync.
+// Uses enterprise confirmation dialog for managed accounts signin outside of
+// the profile picker.
+const base::Feature kAccountPoliciesLoadedWithoutSync{
+ "AccountPoliciesLoadedWithoutSync", base::FEATURE_DISABLED_BY_DEFAULT};
diff --git a/chromium/chrome/browser/signin/signin_features.h b/chromium/chrome/browser/signin/signin_features.h
new file mode 100644
index 00000000000..47c61c21949
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_features.h
@@ -0,0 +1,15 @@
+// Copyright 2020 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_SIGNIN_SIGNIN_FEATURES_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_FEATURES_H_
+
+#include "base/feature_list.h"
+#include "build/chromeos_buildflags.h"
+
+extern const base::Feature kProcessGaiaRemoveLocalAccountHeader;
+
+extern const base::Feature kAccountPoliciesLoadedWithoutSync;
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_FEATURES_H_
diff --git a/chromium/chrome/browser/signin/signin_global_error.cc b/chromium/chrome/browser/signin/signin_global_error.cc
new file mode 100644
index 00000000000..d02d7ad0158
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_global_error.cc
@@ -0,0 +1,170 @@
+// Copyright (c) 2013 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.
+
+#include "chrome/browser/signin/signin_global_error.h"
+
+#include "base/logging.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/ui/browser_commands.h"
+#include "chrome/browser/ui/browser_window.h"
+#include "chrome/browser/ui/chrome_pages.h"
+#include "chrome/browser/ui/global_error/global_error_service.h"
+#include "chrome/browser/ui/global_error/global_error_service_factory.h"
+#include "chrome/browser/ui/singleton_tabs.h"
+#include "chrome/browser/ui/webui/signin/login_ui_service.h"
+#include "chrome/browser/ui/webui/signin/login_ui_service_factory.h"
+#include "chrome/common/url_constants.h"
+#include "chrome/grit/chromium_strings.h"
+#include "chrome/grit/generated_resources.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "net/base/url_util.h"
+#include "ui/base/l10n/l10n_util.h"
+
+#if !defined(OS_ANDROID)
+#include "chrome/browser/signin/signin_promo.h"
+#endif
+
+SigninGlobalError::SigninGlobalError(
+ SigninErrorController* error_controller,
+ Profile* profile)
+ : profile_(profile),
+ error_controller_(error_controller) {
+ error_controller_->AddObserver(this);
+}
+
+SigninGlobalError::~SigninGlobalError() {
+ DCHECK(!error_controller_)
+ << "SigninGlobalError::Shutdown() was not called";
+}
+
+bool SigninGlobalError::HasError() {
+ return HasMenuItem();
+}
+
+void SigninGlobalError::Shutdown() {
+ error_controller_->RemoveObserver(this);
+ error_controller_ = nullptr;
+}
+
+bool SigninGlobalError::HasMenuItem() {
+ return error_controller_->HasError();
+}
+
+int SigninGlobalError::MenuItemCommandID() {
+ return IDC_SHOW_SIGNIN_ERROR;
+}
+
+std::u16string SigninGlobalError::MenuItemLabel() {
+ // Notify the user if there's an auth error the user should know about.
+ if (error_controller_->HasError())
+ return l10n_util::GetStringUTF16(IDS_SYNC_SIGN_IN_ERROR_WRENCH_MENU_ITEM);
+ return std::u16string();
+}
+
+void SigninGlobalError::ExecuteMenuItem(Browser* browser) {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ if (error_controller_->auth_error().state() !=
+ GoogleServiceAuthError::NONE) {
+ DVLOG(1) << "Signing out the user to fix a sync error.";
+ // TODO(beng): seems like this could just call chrome::AttemptUserExit().
+ chrome::ExecuteCommand(browser, IDC_EXIT);
+ return;
+ }
+#endif
+
+ // Global errors don't show up in the wrench menu on mobile.
+#if !defined(OS_ANDROID)
+ LoginUIService* login_ui = LoginUIServiceFactory::GetForProfile(profile_);
+ if (login_ui->current_login_ui()) {
+ login_ui->current_login_ui()->FocusUI();
+ return;
+ }
+
+ browser->window()->ShowAvatarBubbleFromAvatarButton(
+ BrowserWindow::AVATAR_BUBBLE_MODE_REAUTH,
+ signin_metrics::AccessPoint::ACCESS_POINT_MENU, false);
+#endif
+}
+
+bool SigninGlobalError::HasBubbleView() {
+ return !GetBubbleViewMessages().empty();
+}
+
+std::u16string SigninGlobalError::GetBubbleViewTitle() {
+ return l10n_util::GetStringUTF16(IDS_SIGNIN_ERROR_BUBBLE_VIEW_TITLE);
+}
+
+std::vector<std::u16string> SigninGlobalError::GetBubbleViewMessages() {
+ std::vector<std::u16string> messages;
+
+ // If the user isn't signed in, no need to display an error bubble.
+ auto* identity_manager =
+ IdentityManagerFactory::GetForProfileIfExists(profile_);
+ if (identity_manager &&
+ !identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) {
+ return messages;
+ }
+
+ if (!error_controller_->HasError())
+ return messages;
+
+ switch (error_controller_->auth_error().state()) {
+ // TODO(rogerta): use account id in error messages.
+
+ // User credentials are invalid (bad acct, etc).
+ case GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS:
+ case GoogleServiceAuthError::SERVICE_ERROR:
+ messages.push_back(l10n_util::GetStringUTF16(
+ IDS_SYNC_SIGN_IN_ERROR_BUBBLE_VIEW_MESSAGE));
+ break;
+
+ // Sync service is not available for this account's domain.
+ case GoogleServiceAuthError::SERVICE_UNAVAILABLE:
+ messages.push_back(l10n_util::GetStringUTF16(
+ IDS_SYNC_UNAVAILABLE_ERROR_BUBBLE_VIEW_MESSAGE));
+ break;
+
+ // Generic message for "other" errors.
+ default:
+ messages.push_back(l10n_util::GetStringUTF16(
+ IDS_SYNC_OTHER_SIGN_IN_ERROR_BUBBLE_VIEW_MESSAGE));
+ }
+ return messages;
+}
+
+std::u16string SigninGlobalError::GetBubbleViewAcceptButtonLabel() {
+ // If the auth service is unavailable, don't give the user the option to try
+ // signing in again.
+ if (error_controller_->auth_error().state() ==
+ GoogleServiceAuthError::SERVICE_UNAVAILABLE) {
+ return l10n_util::GetStringUTF16(
+ IDS_SYNC_UNAVAILABLE_ERROR_BUBBLE_VIEW_ACCEPT);
+ } else {
+ return l10n_util::GetStringUTF16(IDS_SYNC_SIGN_IN_ERROR_BUBBLE_VIEW_ACCEPT);
+ }
+}
+
+std::u16string SigninGlobalError::GetBubbleViewCancelButtonLabel() {
+ return std::u16string();
+}
+
+void SigninGlobalError::OnBubbleViewDidClose(Browser* browser) {
+}
+
+void SigninGlobalError::BubbleViewAcceptButtonPressed(Browser* browser) {
+ ExecuteMenuItem(browser);
+}
+
+void SigninGlobalError::BubbleViewCancelButtonPressed(Browser* browser) {
+ NOTREACHED();
+}
+
+void SigninGlobalError::OnErrorChanged() {
+ GlobalErrorServiceFactory::GetForProfile(profile_)->NotifyErrorsChanged();
+}
diff --git a/chromium/chrome/browser/signin/signin_global_error.h b/chromium/chrome/browser/signin/signin_global_error.h
new file mode 100644
index 00000000000..50e110ea0bc
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_global_error.h
@@ -0,0 +1,64 @@
+// Copyright (c) 2013 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_SIGNIN_SIGNIN_GLOBAL_ERROR_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_H_
+
+#include <set>
+#include "base/gtest_prod_util.h"
+#include "base/memory/raw_ptr.h"
+#include "chrome/browser/ui/global_error/global_error.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "components/signin/core/browser/signin_error_controller.h"
+
+class Profile;
+
+// Shows auth errors on the wrench menu using a bubble view and a menu item.
+class SigninGlobalError : public GlobalErrorWithStandardBubble,
+ public SigninErrorController::Observer,
+ public KeyedService {
+ public:
+ SigninGlobalError(SigninErrorController* error_controller,
+ Profile* profile);
+
+ SigninGlobalError(const SigninGlobalError&) = delete;
+ SigninGlobalError& operator=(const SigninGlobalError&) = delete;
+
+ ~SigninGlobalError() override;
+
+ // Returns true if there is an authentication error.
+ bool HasError();
+
+ private:
+ FRIEND_TEST_ALL_PREFIXES(SigninGlobalErrorTest, Basic);
+ FRIEND_TEST_ALL_PREFIXES(SigninGlobalErrorTest, AuthStatusEnumerateAllErrors);
+
+ // KeyedService:
+ void Shutdown() override;
+
+ // GlobalErrorWithStandardBubble:
+ bool HasMenuItem() override;
+ int MenuItemCommandID() override;
+ std::u16string MenuItemLabel() override;
+ void ExecuteMenuItem(Browser* browser) override;
+ bool HasBubbleView() override;
+ std::u16string GetBubbleViewTitle() override;
+ std::vector<std::u16string> GetBubbleViewMessages() override;
+ std::u16string GetBubbleViewAcceptButtonLabel() override;
+ std::u16string GetBubbleViewCancelButtonLabel() override;
+ void OnBubbleViewDidClose(Browser* browser) override;
+ void BubbleViewAcceptButtonPressed(Browser* browser) override;
+ void BubbleViewCancelButtonPressed(Browser* browser) override;
+
+ // SigninErrorController::Observer:
+ void OnErrorChanged() override;
+
+ // The Profile this service belongs to.
+ raw_ptr<Profile> profile_;
+
+ // The SigninErrorController that provides auth status.
+ raw_ptr<SigninErrorController> error_controller_;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_H_
diff --git a/chromium/chrome/browser/signin/signin_global_error_factory.cc b/chromium/chrome/browser/signin/signin_global_error_factory.cc
new file mode 100644
index 00000000000..304d6a59cce
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_global_error_factory.cc
@@ -0,0 +1,46 @@
+// Copyright 2014 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.
+
+#include "chrome/browser/signin/signin_global_error_factory.h"
+
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/signin_error_controller_factory.h"
+#include "chrome/browser/signin/signin_global_error.h"
+#include "chrome/browser/ui/global_error/global_error_service_factory.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+
+SigninGlobalErrorFactory::SigninGlobalErrorFactory()
+ : BrowserContextKeyedServiceFactory(
+ "SigninGlobalError",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(SigninErrorControllerFactory::GetInstance());
+ DependsOn(GlobalErrorServiceFactory::GetInstance());
+}
+
+SigninGlobalErrorFactory::~SigninGlobalErrorFactory() {}
+
+// static
+SigninGlobalError* SigninGlobalErrorFactory::GetForProfile(
+ Profile* profile) {
+ return static_cast<SigninGlobalError*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+SigninGlobalErrorFactory* SigninGlobalErrorFactory::GetInstance() {
+ return base::Singleton<SigninGlobalErrorFactory>::get();
+}
+
+KeyedService* SigninGlobalErrorFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ return nullptr;
+#endif
+
+ Profile* profile = static_cast<Profile*>(context);
+ return new SigninGlobalError(
+ SigninErrorControllerFactory::GetForProfile(profile), profile);
+}
diff --git a/chromium/chrome/browser/signin/signin_global_error_factory.h b/chromium/chrome/browser/signin/signin_global_error_factory.h
new file mode 100644
index 00000000000..d13b4f6df61
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_global_error_factory.h
@@ -0,0 +1,40 @@
+// Copyright 2014 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_SIGNIN_SIGNIN_GLOBAL_ERROR_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class SigninGlobalError;
+class Profile;
+
+// Singleton that owns all SigninGlobalErrors and associates them with
+// Profiles. Listens for the Profile's destruction notification and cleans up
+// the associated SigninGlobalError.
+class SigninGlobalErrorFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ // Returns the instance of SigninGlobalError associated with this
+ // profile, creating one if none exists. In Ash, this will return NULL.
+ static SigninGlobalError* GetForProfile(Profile* profile);
+
+ // Returns an instance of the SigninGlobalErrorFactory singleton.
+ static SigninGlobalErrorFactory* GetInstance();
+
+ SigninGlobalErrorFactory(const SigninGlobalErrorFactory&) = delete;
+ SigninGlobalErrorFactory& operator=(const SigninGlobalErrorFactory&) = delete;
+
+ private:
+ friend struct base::DefaultSingletonTraits<SigninGlobalErrorFactory>;
+
+ SigninGlobalErrorFactory();
+ ~SigninGlobalErrorFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/signin_global_error_unittest.cc b/chromium/chrome/browser/signin/signin_global_error_unittest.cc
new file mode 100644
index 00000000000..8d4f5f66eb6
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_global_error_unittest.cc
@@ -0,0 +1,166 @@
+// Copyright (c) 2013 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.
+
+#include "chrome/browser/signin/signin_global_error.h"
+
+#include <stddef.h>
+
+#include <memory>
+#include <string>
+
+#include "base/bind.h"
+#include "base/cxx17_backports.h"
+#include "base/memory/raw_ptr.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/profiles/profile_metrics.h"
+#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
+#include "chrome/browser/signin/signin_error_controller_factory.h"
+#include "chrome/browser/signin/signin_global_error_factory.h"
+#include "chrome/browser/ui/global_error/global_error_service.h"
+#include "chrome/browser/ui/global_error/global_error_service_factory.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "chrome/test/base/testing_profile.h"
+#include "chrome/test/base/testing_profile_manager.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/core/browser/signin_error_controller.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "components/sync_preferences/pref_service_syncable.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+static const char kTestEmail[] = "testuser@test.com";
+static const char16_t kTestEmail16[] = u"testuser@test.com";
+
+class SigninGlobalErrorTest : public testing::Test {
+ public:
+ SigninGlobalErrorTest() :
+ profile_manager_(TestingBrowserProcess::GetGlobal()) {}
+
+ void SetUp() override {
+ ASSERT_TRUE(profile_manager_.SetUp());
+
+ // Create a signed-in profile.
+ TestingProfile::TestingFactories testing_factories =
+ IdentityTestEnvironmentProfileAdaptor::
+ GetIdentityTestEnvironmentFactories();
+
+ profile_ = profile_manager_.CreateTestingProfile(
+ "Person 1", std::unique_ptr<sync_preferences::PrefServiceSyncable>(),
+ u"Person 1", 0, std::string(), std::move(testing_factories));
+
+ identity_test_env_profile_adaptor_ =
+ std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile());
+
+ AccountInfo account_info =
+ identity_test_env_profile_adaptor_->identity_test_env()
+ ->MakePrimaryAccountAvailable(kTestEmail,
+ signin::ConsentLevel::kSync);
+ ProfileAttributesEntry* entry =
+ profile_manager_.profile_attributes_storage()
+ ->GetProfileAttributesWithPath(profile()->GetPath());
+ ASSERT_NE(entry, nullptr);
+
+ entry->SetAuthInfo(account_info.gaia, kTestEmail16,
+ /*is_consented_primary_account=*/true);
+
+ global_error_ = SigninGlobalErrorFactory::GetForProfile(profile());
+ error_controller_ = SigninErrorControllerFactory::GetForProfile(profile());
+ }
+
+ TestingProfile* profile() { return profile_; }
+ TestingProfileManager* testing_profile_manager() {
+ return &profile_manager_;
+ }
+
+ SigninGlobalError* global_error() { return global_error_; }
+ SigninErrorController* error_controller() { return error_controller_; }
+
+ void SetAuthError(GoogleServiceAuthError::State state) {
+ signin::IdentityTestEnvironment* identity_test_env =
+ identity_test_env_profile_adaptor_->identity_test_env();
+ CoreAccountId primary_account_id =
+ identity_test_env->identity_manager()->GetPrimaryAccountId(
+ signin::ConsentLevel::kSync);
+
+ signin::UpdatePersistentErrorOfRefreshTokenForAccount(
+ identity_test_env->identity_manager(), primary_account_id,
+ GoogleServiceAuthError(state));
+ }
+
+ private:
+ content::BrowserTaskEnvironment task_environment_;
+ TestingProfileManager profile_manager_;
+ raw_ptr<TestingProfile> profile_;
+
+ std::unique_ptr<IdentityTestEnvironmentProfileAdaptor>
+ identity_test_env_profile_adaptor_;
+
+ raw_ptr<SigninGlobalError> global_error_;
+ raw_ptr<SigninErrorController> error_controller_;
+};
+
+TEST_F(SigninGlobalErrorTest, Basic) {
+ ASSERT_FALSE(global_error()->HasMenuItem());
+
+ SetAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS);
+ EXPECT_TRUE(global_error()->HasMenuItem());
+
+ SetAuthError(GoogleServiceAuthError::NONE);
+ EXPECT_FALSE(global_error()->HasMenuItem());
+}
+
+// Verify that SigninGlobalError ignores certain errors.
+TEST_F(SigninGlobalErrorTest, AuthStatusEnumerateAllErrors) {
+ typedef struct {
+ GoogleServiceAuthError::State error_state;
+ bool is_error;
+ } ErrorTableEntry;
+
+ ErrorTableEntry table[] = {
+ {GoogleServiceAuthError::NONE, false},
+ {GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS, true},
+ {GoogleServiceAuthError::USER_NOT_SIGNED_UP, true},
+ {GoogleServiceAuthError::CONNECTION_FAILED, false},
+ {GoogleServiceAuthError::SERVICE_UNAVAILABLE, false},
+ {GoogleServiceAuthError::REQUEST_CANCELED, false},
+ {GoogleServiceAuthError::UNEXPECTED_SERVICE_RESPONSE, true},
+ {GoogleServiceAuthError::SERVICE_ERROR, true},
+ };
+ static_assert(
+ base::size(table) == GoogleServiceAuthError::NUM_STATES -
+ GoogleServiceAuthError::kDeprecatedStateCount,
+ "table size should match number of auth error types");
+
+ // Mark the profile with an active timestamp so profile_metrics logs it.
+ testing_profile_manager()->UpdateLastUser(profile());
+
+ for (ErrorTableEntry entry : table) {
+ SetAuthError(GoogleServiceAuthError::NONE);
+
+ base::HistogramTester histogram_tester;
+ SetAuthError(entry.error_state);
+
+ EXPECT_EQ(global_error()->HasMenuItem(), entry.is_error);
+ EXPECT_EQ(global_error()->MenuItemLabel().empty(), !entry.is_error);
+ EXPECT_EQ(global_error()->GetBubbleViewMessages().empty(), !entry.is_error);
+ EXPECT_FALSE(global_error()->GetBubbleViewTitle().empty());
+ EXPECT_FALSE(global_error()->GetBubbleViewAcceptButtonLabel().empty());
+ EXPECT_TRUE(global_error()->GetBubbleViewCancelButtonLabel().empty());
+
+ ProfileMetrics::LogNumberOfProfiles(&testing_profile_manager()
+ ->profile_manager()
+ ->GetProfileAttributesStorage());
+
+ if (entry.is_error) {
+ histogram_tester.ExpectBucketCount("Signin.AuthError", entry.error_state,
+ 1);
+ }
+ }
+}
diff --git a/chromium/chrome/browser/signin/signin_manager.cc b/chromium/chrome/browser/signin/signin_manager.cc
new file mode 100644
index 00000000000..9cc73e0e49e
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_manager.cc
@@ -0,0 +1,200 @@
+// Copyright 2020 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.
+
+#include "chrome/browser/signin/signin_manager.h"
+
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/primary_account_mutator.h"
+
+SigninManager::SigninManager(PrefService* prefs,
+ signin::IdentityManager* identity_manager)
+ : prefs_(prefs), identity_manager_(identity_manager) {
+ signin_allowed_.Init(
+ prefs::kSigninAllowed, prefs_,
+ base::BindRepeating(&SigninManager::OnSigninAllowedPrefChanged,
+ base::Unretained(this)));
+
+ UpdateUnconsentedPrimaryAccount();
+ identity_manager_->AddObserver(this);
+}
+
+SigninManager::~SigninManager() {
+ identity_manager_->RemoveObserver(this);
+}
+
+void SigninManager::UpdateUnconsentedPrimaryAccount() {
+ // Only update the unconsented primary account only after accounts are loaded.
+ if (!identity_manager_->AreRefreshTokensLoaded()) {
+ return;
+ }
+
+ absl::optional<CoreAccountInfo> account =
+ ComputeUnconsentedPrimaryAccountInfo();
+
+ DCHECK(!account || !account->IsEmpty());
+ if (account) {
+ if (identity_manager_->GetPrimaryAccountInfo(
+ signin::ConsentLevel::kSignin) != account) {
+ DCHECK(
+ !identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ identity_manager_->GetPrimaryAccountMutator()->SetPrimaryAccount(
+ account->account_id, signin::ConsentLevel::kSignin);
+ }
+ } else if (identity_manager_->HasPrimaryAccount(
+ signin::ConsentLevel::kSignin)) {
+ DCHECK(!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ identity_manager_->GetPrimaryAccountMutator()->ClearPrimaryAccount(
+ signin_metrics::USER_DELETED_ACCOUNT_COOKIES,
+ signin_metrics::SignoutDelete::kIgnoreMetric);
+ }
+}
+
+absl::optional<CoreAccountInfo>
+SigninManager::ComputeUnconsentedPrimaryAccountInfo() const {
+ DCHECK(identity_manager_->AreRefreshTokensLoaded());
+
+ // UPA is equal to the primary account with sync consent if it exists.
+ if (identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync)) {
+ return identity_manager_->GetPrimaryAccountInfo(
+ signin::ConsentLevel::kSync);
+ }
+
+ // Clearing the primary sync account when sign-in is not allowed is handled
+ // by PrimaryAccountPolicyManager. That flow is extremely hard to follow
+ // especially for the case when the user is syncing with a managed account
+ // as in that case the whole profile needs to be deleted.
+ //
+ // It was considered simpler to keep the logic to update the unconsented
+ // primary account in a single place.
+ if (!signin_allowed_.GetValue())
+ return absl::nullopt;
+
+ signin::AccountsInCookieJarInfo cookie_info =
+ identity_manager_->GetAccountsInCookieJar();
+
+ std::vector<gaia::ListedAccount> cookie_accounts =
+ cookie_info.signed_in_accounts;
+
+ // Fresh cookies and loaded tokens are needed to compute the UPA.
+ if (cookie_info.accounts_are_fresh) {
+ // Cookies are fresh and tokens are loaded, UPA is the first account
+ // in cookies if it exists and has a refresh token.
+ if (cookie_accounts.empty()) {
+ // Cookies are empty, the UPA is empty.
+ return absl::nullopt;
+ }
+
+ AccountInfo account_info =
+ identity_manager_->FindExtendedAccountInfoByAccountId(
+ cookie_accounts[0].id);
+
+ // Verify the first account in cookies has a refresh token that is valid.
+ bool error_state =
+ account_info.IsEmpty() ||
+ identity_manager_->HasAccountWithRefreshTokenInPersistentErrorState(
+ account_info.account_id);
+
+ return error_state ? absl::nullopt
+ : absl::make_optional<CoreAccountInfo>(account_info);
+ }
+
+ if (!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin))
+ return absl::nullopt;
+
+ // If cookies or tokens are not loaded, it is not possible to fully compute
+ // the unconsented primary account. However, if the current unconsented
+ // primary account is no longer valid, it has to be removed.
+ CoreAccountId current_account =
+ identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSignin);
+
+ if (!identity_manager_->HasAccountWithRefreshToken(current_account)) {
+ // Tokens are loaded, but the current UPA doesn't have a refresh token.
+ // Clear the current UPA.
+ return absl::nullopt;
+ }
+
+ if (cookie_info.accounts_are_fresh) {
+ if (cookie_accounts.empty() || cookie_accounts[0].id != current_account) {
+ // The current UPA is not the first in fresh cookies. It needs to be
+ // cleared.
+ return absl::nullopt;
+ }
+ }
+
+ // No indication that the current UPA is invalid, return current UPA.
+ return identity_manager_->GetPrimaryAccountInfo(
+ signin::ConsentLevel::kSignin);
+}
+
+// signin::IdentityManager::Observer implementation.
+void SigninManager::OnPrimaryAccountChanged(
+ const signin::PrimaryAccountChangeEvent& event_details) {
+ // This is needed for the case where the user chooses to start syncing
+ // with an account that is different from the unconsented primary account
+ // (not the first in cookies) but then cancels. In that case, the tokens stay
+ // the same. In all the other cases, either the token will be revoked which
+ // will trigger an update for the unconsented primary account or the
+ // primary account stays the same but the sync consent is revoked.
+ if (event_details.GetEventTypeFor(signin::ConsentLevel::kSync) !=
+ signin::PrimaryAccountChangeEvent::Type::kCleared) {
+ return;
+ }
+
+ // It is important to update the primary account after all observers process
+ // the current OnPrimaryAccountChanged() as all observers should see the same
+ // value for the unconsented primary account. Schedule the potential update
+ // on the next run loop.
+ base::SequencedTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::BindOnce(&SigninManager::UpdateUnconsentedPrimaryAccount,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+void SigninManager::OnRefreshTokenUpdatedForAccount(
+ const CoreAccountInfo& account_info) {
+ UpdateUnconsentedPrimaryAccount();
+}
+
+void SigninManager::OnRefreshTokenRemovedForAccount(
+ const CoreAccountId& account_id) {
+ UpdateUnconsentedPrimaryAccount();
+}
+
+void SigninManager::OnRefreshTokensLoaded() {
+ UpdateUnconsentedPrimaryAccount();
+}
+
+void SigninManager::OnAccountsInCookieUpdated(
+ const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info,
+ const GoogleServiceAuthError& error) {
+ UpdateUnconsentedPrimaryAccount();
+}
+
+void SigninManager::OnAccountsCookieDeletedByUserAction() {
+ UpdateUnconsentedPrimaryAccount();
+}
+
+void SigninManager::OnErrorStateOfRefreshTokenUpdatedForAccount(
+ const CoreAccountInfo& account_info,
+ const GoogleServiceAuthError& error) {
+ CoreAccountInfo current_account =
+ identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
+
+ bool should_update = false;
+ if (error == GoogleServiceAuthError::AuthErrorNone()) {
+ should_update = current_account.IsEmpty();
+ } else {
+ // In error state, update if the account in error is the current UPA.
+ should_update = (account_info == current_account);
+ }
+
+ if (should_update)
+ UpdateUnconsentedPrimaryAccount();
+}
+
+void SigninManager::OnSigninAllowedPrefChanged() {
+ UpdateUnconsentedPrimaryAccount();
+}
diff --git a/chromium/chrome/browser/signin/signin_manager.h b/chromium/chrome/browser/signin/signin_manager.h
new file mode 100644
index 00000000000..2feaf8dc737
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_manager.h
@@ -0,0 +1,71 @@
+// Copyright 2020 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_SIGNIN_SIGNIN_MANAGER_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_H_
+
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "components/prefs/pref_member.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+
+class PrefService;
+
+class SigninManager : public KeyedService,
+ public signin::IdentityManager::Observer {
+ public:
+ SigninManager(PrefService* prefs, signin::IdentityManager* identity_manger);
+ SigninManager(const SigninManager&) = delete;
+ SigninManager& operator=(const SigninManager&) = delete;
+
+ ~SigninManager() override;
+
+ private:
+ // Updates the cached version of unconsented primary account and notifies the
+ // observers if there is any change.
+ void UpdateUnconsentedPrimaryAccount();
+
+ // Computes and returns the unconsented primary account (UPA).
+ // - If a primary account with sync consent exists, the UPA is equal to it.
+ // - The UPA is the first account in cookies and must have a refresh token.
+ // For the UPA to be computed, it needs fresh cookies and tokens to be loaded.
+ // - If tokens are not loaded or cookies are not fresh, the UPA can't be
+ // computed but if one already exists it might be invalid. That can happen if
+ // cookies are fresh but are empty or the first account is different than the
+ // current UPA, the other cases are if tokens are not loaded but the current
+ // UPA's refresh token has been rekoved or tokens are loaded but the current
+ // UPA does not have a refresh token. If the UPA is invalid, it needs to be
+ // cleared, |absl::nullopt| is returned. If it is still valid, returns the
+ // valid UPA.
+ absl::optional<CoreAccountInfo> ComputeUnconsentedPrimaryAccountInfo() const;
+
+ // signin::IdentityManager::Observer implementation.
+ void OnPrimaryAccountChanged(
+ const signin::PrimaryAccountChangeEvent& event_details) override;
+ void OnRefreshTokenUpdatedForAccount(
+ const CoreAccountInfo& account_info) override;
+ void OnRefreshTokenRemovedForAccount(
+ const CoreAccountId& account_id) override;
+ void OnRefreshTokensLoaded() override;
+ void OnAccountsInCookieUpdated(
+ const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info,
+ const GoogleServiceAuthError& error) override;
+ void OnAccountsCookieDeletedByUserAction() override;
+ void OnErrorStateOfRefreshTokenUpdatedForAccount(
+ const CoreAccountInfo& account_info,
+ const GoogleServiceAuthError& error) override;
+
+ void OnSigninAllowedPrefChanged();
+
+ raw_ptr<PrefService> prefs_;
+ raw_ptr<signin::IdentityManager> identity_manager_;
+
+ // Helper object to listen for changes to the signin allowed preference.
+ BooleanPrefMember signin_allowed_;
+
+ base::WeakPtrFactory<SigninManager> weak_ptr_factory_{this};
+};
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_H_
diff --git a/chromium/chrome/browser/signin/signin_manager_android_factory.cc b/chromium/chrome/browser/signin/signin_manager_android_factory.cc
new file mode 100644
index 00000000000..08d545c346a
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_manager_android_factory.cc
@@ -0,0 +1,41 @@
+// Copyright 2019 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.
+
+#include "chrome/browser/signin/signin_manager_android_factory.h"
+
+#include "chrome/browser/android/signin/signin_manager_android.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+
+SigninManagerAndroidFactory::SigninManagerAndroidFactory()
+ : BrowserContextKeyedServiceFactory(
+ "SigninManagerAndroid",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(IdentityManagerFactory::GetInstance());
+}
+
+SigninManagerAndroidFactory::~SigninManagerAndroidFactory() {}
+
+// static
+base::android::ScopedJavaLocalRef<jobject>
+SigninManagerAndroidFactory::GetJavaObjectForProfile(Profile* profile) {
+ return static_cast<SigninManagerAndroid*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true))
+ ->GetJavaObject();
+}
+
+// static
+SigninManagerAndroidFactory* SigninManagerAndroidFactory::GetInstance() {
+ static base::NoDestructor<SigninManagerAndroidFactory> instance;
+ return instance.get();
+}
+
+KeyedService* SigninManagerAndroidFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
+
+ return new SigninManagerAndroid(profile, identity_manager);
+}
diff --git a/chromium/chrome/browser/signin/signin_manager_android_factory.h b/chromium/chrome/browser/signin/signin_manager_android_factory.h
new file mode 100644
index 00000000000..5eabc40186a
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_manager_android_factory.h
@@ -0,0 +1,32 @@
+// Copyright 2019 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_SIGNIN_SIGNIN_MANAGER_ANDROID_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_ANDROID_FACTORY_H_
+
+#include "base/android/scoped_java_ref.h"
+#include "base/no_destructor.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class Profile;
+
+class SigninManagerAndroidFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ static base::android::ScopedJavaLocalRef<jobject> GetJavaObjectForProfile(
+ Profile* profile);
+
+ // Returns an instance of the SigninManagerAndroidFactory singleton.
+ static SigninManagerAndroidFactory* GetInstance();
+
+ private:
+ friend class base::NoDestructor<SigninManagerAndroidFactory>;
+ SigninManagerAndroidFactory();
+
+ ~SigninManagerAndroidFactory() override;
+
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* context) const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_ANDROID_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/signin_manager_factory.cc b/chromium/chrome/browser/signin/signin_manager_factory.cc
new file mode 100644
index 00000000000..788dbee7f54
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_manager_factory.cc
@@ -0,0 +1,48 @@
+// Copyright 2020 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.
+
+#include "chrome/browser/signin/signin_manager_factory.h"
+
+#include "base/logging.h"
+#include "build/build_config.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+
+// static
+SigninManagerFactory* SigninManagerFactory::GetInstance() {
+ return base::Singleton<SigninManagerFactory>::get();
+}
+
+// static
+SigninManager* SigninManagerFactory::GetForProfile(Profile* profile) {
+ DCHECK(profile);
+ return static_cast<SigninManager*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+SigninManagerFactory::SigninManagerFactory()
+ : BrowserContextKeyedServiceFactory(
+ "SigninManager",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(IdentityManagerFactory::GetInstance());
+}
+
+SigninManagerFactory::~SigninManagerFactory() = default;
+
+KeyedService* SigninManagerFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ return new SigninManager(profile->GetPrefs(),
+ IdentityManagerFactory::GetForProfile(profile));
+}
+
+bool SigninManagerFactory::ServiceIsCreatedWithBrowserContext() const {
+ return true;
+}
+
+bool SigninManagerFactory::ServiceIsNULLWhileTesting() const {
+ return true;
+}
diff --git a/chromium/chrome/browser/signin/signin_manager_factory.h b/chromium/chrome/browser/signin/signin_manager_factory.h
new file mode 100644
index 00000000000..d0ca640e22d
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_manager_factory.h
@@ -0,0 +1,33 @@
+// Copyright 2020 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_SIGNIN_SIGNIN_MANAGER_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/signin_manager.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class SigninManagerFactory : public BrowserContextKeyedServiceFactory {
+ public:
+ // Returns an instance of the factory singleton.
+ static SigninManagerFactory* GetInstance();
+
+ static SigninManager* GetForProfile(Profile* profile);
+
+ private:
+ friend struct base::DefaultSingletonTraits<SigninManagerFactory>;
+
+ SigninManagerFactory();
+ ~SigninManagerFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* context) const override;
+ bool ServiceIsCreatedWithBrowserContext() const override;
+ bool ServiceIsNULLWhileTesting() const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/signin_manager_unittest.cc b/chromium/chrome/browser/signin/signin_manager_unittest.cc
new file mode 100644
index 00000000000..e2bb33af9f0
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_manager_unittest.cc
@@ -0,0 +1,421 @@
+// Copyright 2020 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.
+#include "chrome/browser/signin/signin_manager.h"
+
+#include "base/memory/raw_ptr.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "components/sync_preferences/testing_pref_service_syncable.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using ::testing::_;
+using ::testing::Mock;
+
+namespace signin {
+namespace {
+const char kTestEmail[] = "me@gmail.com";
+const char kTestEmail2[] = "me2@gmail.com";
+
+class FakeIdentityManagerObserver : public IdentityManager::Observer {
+ public:
+ explicit FakeIdentityManagerObserver(IdentityManager* identity_manager)
+ : identity_manager_(identity_manager) {}
+ ~FakeIdentityManagerObserver() override = default;
+
+ void OnPrimaryAccountChanged(
+ const PrimaryAccountChangeEvent& event) override {
+ auto current_state = event.GetCurrentState();
+ EXPECT_EQ(
+ current_state.primary_account,
+ identity_manager_->GetPrimaryAccountInfo(current_state.consent_level));
+ events_.push_back(event);
+ }
+
+ const std::vector<PrimaryAccountChangeEvent>& events() const {
+ return events_;
+ }
+
+ void Reset() { events_.clear(); }
+
+ private:
+ raw_ptr<IdentityManager> identity_manager_;
+ std::vector<PrimaryAccountChangeEvent> events_;
+};
+} // namespace
+
+class SigninManagerTest : public testing::Test {
+ public:
+ SigninManagerTest()
+ : identity_test_env_(/*test_url_loader_factory=*/nullptr,
+ /*pref_service=*/&prefs_,
+ signin::AccountConsistencyMethod::kDice,
+ /*test_signin_client=*/nullptr),
+ observer_(identity_test_env_.identity_manager()) {}
+
+ SigninManagerTest(const SigninManagerTest&) = delete;
+ SigninManagerTest& operator=(const SigninManagerTest&) = delete;
+
+ void SetUp() override {
+ testing::Test::SetUp();
+ RecreateSigninManager();
+ identity_manager()->AddObserver(&observer_);
+ }
+
+ void TearDown() override { identity_manager()->RemoveObserver(&observer_); }
+
+ void RecreateSigninManager() {
+ signin_manger_ =
+ std::make_unique<SigninManager>(&prefs_, identity_manager());
+ }
+
+ AccountInfo GetAccountInfo(const std::string& email) {
+ AccountInfo account_info;
+ account_info.gaia = GetTestGaiaIdForEmail(email);
+ account_info.account_id =
+ identity_manager()->PickAccountIdForAccount(account_info.gaia, email);
+ account_info.email = email;
+ return account_info;
+ }
+
+ void ExpectUnconsentedPrimaryAccountSetEvent(
+ const CoreAccountInfo& expected_primary_account) {
+ EXPECT_EQ(1U, observer().events().size());
+ auto event = observer().events()[0];
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet,
+ event.GetEventTypeFor(ConsentLevel::kSignin));
+ EXPECT_TRUE(event.GetPreviousState().primary_account.IsEmpty());
+ EXPECT_EQ(expected_primary_account,
+ event.GetCurrentState().primary_account);
+ observer().Reset();
+ }
+
+ void ExpectUnconsentedPrimaryAccountClearedEvent(
+ const CoreAccountInfo& expected_cleared_account) {
+ EXPECT_EQ(1U, observer().events().size());
+ auto event = observer().events()[0];
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared,
+ event.GetEventTypeFor(ConsentLevel::kSignin));
+ EXPECT_EQ(expected_cleared_account,
+ event.GetPreviousState().primary_account);
+ EXPECT_TRUE(event.GetCurrentState().primary_account.IsEmpty());
+ observer().Reset();
+ }
+
+ void ExpectSyncPrimaryAccountSetEvent(
+ const CoreAccountInfo& expected_primary_account) {
+ EXPECT_EQ(1U, observer().events().size());
+ auto event = observer().events()[0];
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet,
+ event.GetEventTypeFor(ConsentLevel::kSignin));
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet,
+ event.GetEventTypeFor(ConsentLevel::kSync));
+ EXPECT_TRUE(event.GetPreviousState().primary_account.IsEmpty());
+ EXPECT_EQ(expected_primary_account,
+ event.GetCurrentState().primary_account);
+ observer().Reset();
+ }
+
+ IdentityManager* identity_manager() {
+ return identity_test_env_.identity_manager();
+ }
+
+ IdentityTestEnvironment* identity_test_env() { return &identity_test_env_; }
+
+ AccountInfo MakeAccountAvailableWithCookies(const std::string& email) {
+ AccountInfo account = GetAccountInfo(kTestEmail);
+ identity_test_env_.MakeAccountAvailableWithCookies(account.email,
+ account.gaia);
+ EXPECT_FALSE(account.IsEmpty());
+ EXPECT_TRUE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin));
+ EXPECT_EQ(account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync));
+ return account;
+ }
+
+ AccountInfo MakeSyncAccountAvailableWithCookies(const std::string& email) {
+ AccountInfo account = identity_test_env_.MakePrimaryAccountAvailable(
+ email, signin::ConsentLevel::kSync);
+ identity_test_env_.SetCookieAccounts({{account.email, account.gaia}});
+ EXPECT_EQ(account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ EXPECT_EQ(account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSync));
+ EXPECT_TRUE(identity_manager()->HasPrimaryAccountWithRefreshToken(
+ signin::ConsentLevel::kSync));
+ return account;
+ }
+
+ FakeIdentityManagerObserver& observer() { return observer_; }
+
+ sync_preferences::TestingPrefServiceSyncable prefs_;
+ content::BrowserTaskEnvironment task_environment_;
+ IdentityTestEnvironment identity_test_env_;
+ std::unique_ptr<SigninManager> signin_manger_;
+ FakeIdentityManagerObserver observer_;
+};
+
+TEST_F(
+ SigninManagerTest,
+ UnconsentedPrimaryAccountUpdatedOnItsAccountRefreshTokenUpdateWithValidTokenWhenNoSyncConsent) {
+ // Add an unconsented primary account, incl. proper cookies.
+ AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail);
+ ExpectUnconsentedPrimaryAccountSetEvent(account);
+ EXPECT_EQ(account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+}
+
+TEST_F(
+ SigninManagerTest,
+ UnconsentedPrimaryAccountUpdatedOnItsAccountRefreshTokenUpdateWithInvalidTokenWhenNoSyncConsent) {
+ // Prerequisite: add an unconsented primary account, incl. proper cookies.
+ AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail);
+ ExpectUnconsentedPrimaryAccountSetEvent(account);
+
+ // Invalid token.
+ SetInvalidRefreshTokenForAccount(identity_manager(), account.account_id);
+ ExpectUnconsentedPrimaryAccountClearedEvent(account);
+ EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin));
+
+ // Update with a valid token.
+ SetRefreshTokenForAccount(identity_manager(), account.account_id, "");
+ ExpectUnconsentedPrimaryAccountSetEvent(account);
+ EXPECT_EQ(identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin),
+ account);
+}
+
+TEST_F(
+ SigninManagerTest,
+ UnconsentedPrimaryAccountRemovedOnItsAccountRefreshTokenRemovalWhenNoSyncConsent) {
+ // Prerequisite: Add an unconsented primary account, incl. proper cookies.
+ AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail);
+ ExpectUnconsentedPrimaryAccountSetEvent(account);
+
+ // With no refresh token, there is no unconsented primary account any more.
+ identity_test_env()->RemoveRefreshTokenForAccount(account.account_id);
+ ExpectUnconsentedPrimaryAccountClearedEvent(account);
+ EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin));
+}
+
+TEST_F(SigninManagerTest, UnconsentedPrimaryAccountNotChangedOnSignout) {
+ // Set a primary account at sync consent level.
+ AccountInfo account = MakeSyncAccountAvailableWithCookies(kTestEmail);
+ EXPECT_EQ(account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ EXPECT_EQ(account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSync));
+ EXPECT_TRUE(identity_manager()->HasPrimaryAccountWithRefreshToken(
+ signin::ConsentLevel::kSync));
+
+ // Verify the primary account changed event.
+ ExpectSyncPrimaryAccountSetEvent(account);
+
+ // Tests that sync primary account is cleared, but unconsented account is not.
+ identity_test_env()->RevokeSyncConsent();
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_EQ(account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync));
+
+ EXPECT_EQ(1U, observer().events().size());
+ auto event = observer().events()[0];
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kNone,
+ event.GetEventTypeFor(ConsentLevel::kSignin));
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared,
+ event.GetEventTypeFor(ConsentLevel::kSync));
+ EXPECT_EQ(account, event.GetPreviousState().primary_account);
+ EXPECT_EQ(account, event.GetCurrentState().primary_account);
+}
+
+TEST_F(SigninManagerTest,
+ UnconsentedPrimaryAccountTokenRevokedWithStaleCookies) {
+ // Prerequisite: add an unconsented primary account, incl. proper cookies.
+ AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail);
+ ExpectUnconsentedPrimaryAccountSetEvent(account);
+
+ // Make the cookies stale and remove the account.
+ // Removing the refresh token for the unconsented primary account is
+ // sufficient to clear it.
+ identity_test_env()->SetFreshnessOfAccountsInGaiaCookie(false);
+ identity_test_env()->RemoveRefreshTokenForAccount(account.account_id);
+ ASSERT_FALSE(identity_manager()->GetAccountsInCookieJar().accounts_are_fresh);
+
+ // Unconsented account was removed.
+ EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin));
+ ExpectUnconsentedPrimaryAccountClearedEvent(account);
+}
+
+TEST_F(SigninManagerTest,
+ UnconsentedPrimaryAccountTokenRevokedWithStaleCookiesMultipleAccounts) {
+ // Add two accounts with cookies.
+ AccountInfo main_account =
+ identity_test_env()->MakeAccountAvailable(kTestEmail);
+ AccountInfo secondary_account =
+ identity_test_env()->MakeAccountAvailable(kTestEmail2);
+ identity_test_env()->SetCookieAccounts(
+ {{main_account.email, main_account.gaia},
+ {secondary_account.email, secondary_account.gaia}});
+
+ EXPECT_TRUE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin));
+ EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync));
+ EXPECT_EQ(main_account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ ExpectUnconsentedPrimaryAccountSetEvent(main_account);
+
+ // Make the cookies stale and remove the main account.
+ identity_test_env()->SetFreshnessOfAccountsInGaiaCookie(false);
+ identity_test_env()->RemoveRefreshTokenForAccount(main_account.account_id);
+ ASSERT_FALSE(identity_manager()->GetAccountsInCookieJar().accounts_are_fresh);
+
+ // Unconsented account was removed.
+ EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin));
+ ExpectUnconsentedPrimaryAccountClearedEvent(main_account);
+}
+
+TEST_F(SigninManagerTest, UnconsentedPrimaryAccountDuringLoad) {
+ // Pre-requisite: Add two accounts with cookies.
+ AccountInfo main_account =
+ identity_test_env()->MakeAccountAvailable(kTestEmail);
+ AccountInfo secondary_account =
+ identity_test_env()->MakeAccountAvailable(kTestEmail2);
+ identity_test_env()->SetCookieAccounts(
+ {{main_account.email, main_account.gaia},
+ {secondary_account.email, secondary_account.gaia}});
+ ASSERT_EQ(main_account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ ASSERT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync));
+ ExpectUnconsentedPrimaryAccountSetEvent(main_account);
+
+ // Set the token service in "loading" mode.
+ identity_test_env()->ResetToAccountsNotYetLoadedFromDiskState();
+ RecreateSigninManager();
+
+ // Unconsented primary account is available while tokens are not loaded.
+ EXPECT_EQ(main_account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ EXPECT_TRUE(observer().events().empty());
+
+ // Revoking an unrelated token doesn't change the unconsented primary account.
+ identity_test_env()->RemoveRefreshTokenForAccount(
+ secondary_account.account_id);
+ EXPECT_EQ(main_account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ EXPECT_TRUE(observer().events().empty());
+
+ // Revoking the token of the unconsented primary account while the tokens
+ // are still loading does not change the unconsented primary account.
+ identity_test_env()->RemoveRefreshTokenForAccount(main_account.account_id);
+ EXPECT_EQ(main_account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ EXPECT_TRUE(observer().events().empty());
+
+ // Finish the token load should clear the primary account as the token of the
+ // primary account was revoked.
+ identity_test_env()->ReloadAccountsFromDisk();
+ EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin));
+ ExpectUnconsentedPrimaryAccountClearedEvent(main_account);
+}
+
+TEST_F(SigninManagerTest,
+ UnconsentedPrimaryAccountUpdatedOnSyncConsentRevoked) {
+ AccountInfo first_account =
+ identity_test_env()->MakeAccountAvailable(kTestEmail);
+ AccountInfo second_account =
+ identity_test_env()->MakeAccountAvailable(kTestEmail2);
+ identity_test_env()->SetCookieAccounts(
+ {{first_account.email, first_account.gaia},
+ {second_account.email, second_account.gaia}});
+ ASSERT_EQ(first_account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+ ExpectUnconsentedPrimaryAccountSetEvent(first_account);
+
+ // Set the sync primary account to the second account in cookies.
+ // The unconsented primary account should be updated.
+ identity_test_env()->SetPrimaryAccount(second_account.email,
+ signin::ConsentLevel::kSync);
+ EXPECT_EQ(second_account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSync));
+ EXPECT_EQ(1U, observer().events().size());
+ auto event = observer().events()[0];
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet,
+ event.GetEventTypeFor(ConsentLevel::kSync));
+ EXPECT_EQ(first_account, event.GetPreviousState().primary_account);
+ EXPECT_EQ(second_account, event.GetCurrentState().primary_account);
+ observer().Reset();
+
+ // Clear primary account but do not delete the account. The unconsented
+ // primary account should be updated to be the first account in cookies.
+ identity_test_env()->RevokeSyncConsent();
+ base::RunLoop().RunUntilIdle();
+
+ // Primary account is cleared, but unconsented account is not.
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync));
+ EXPECT_TRUE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin));
+ EXPECT_EQ(first_account,
+ identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin));
+
+ EXPECT_EQ(2U, observer().events().size());
+ event = observer().events()[0];
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared,
+ event.GetEventTypeFor(ConsentLevel::kSync));
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kNone,
+ event.GetEventTypeFor(ConsentLevel::kSignin));
+ EXPECT_EQ(second_account, event.GetPreviousState().primary_account);
+ EXPECT_EQ(second_account, event.GetCurrentState().primary_account);
+
+ event = observer().events()[1];
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kNone,
+ event.GetEventTypeFor(ConsentLevel::kSync));
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet,
+ event.GetEventTypeFor(ConsentLevel::kSignin));
+ EXPECT_EQ(second_account, event.GetPreviousState().primary_account);
+ EXPECT_EQ(first_account, event.GetCurrentState().primary_account);
+}
+
+TEST_F(SigninManagerTest, ClearPrimaryAccountAndSignOut) {
+ AccountInfo account = MakeSyncAccountAvailableWithCookies(kTestEmail);
+ ExpectSyncPrimaryAccountSetEvent(account);
+
+ identity_test_env()->ClearPrimaryAccount();
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_EQ(1U, observer().events().size());
+ auto event = observer().events()[0];
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared,
+ event.GetEventTypeFor(ConsentLevel::kSignin));
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared,
+ event.GetEventTypeFor(ConsentLevel::kSync));
+ EXPECT_EQ(account, event.GetPreviousState().primary_account);
+ EXPECT_TRUE(event.GetCurrentState().primary_account.IsEmpty());
+}
+
+TEST_F(SigninManagerTest,
+ UnconsentedPrimaryAccountClearedWhenSigninDisallowed) {
+ // Prerequisite: add an unconsented primary account.
+ AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail);
+ ExpectUnconsentedPrimaryAccountSetEvent(account);
+
+ prefs_.SetBoolean(prefs::kSigninAllowed, false);
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_FALSE(
+ identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
+
+ EXPECT_EQ(1U, observer().events().size());
+ auto event = observer().events()[0];
+ EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared,
+ event.GetEventTypeFor(ConsentLevel::kSignin));
+ EXPECT_EQ(account, event.GetPreviousState().primary_account);
+ EXPECT_TRUE(event.GetCurrentState().primary_account.IsEmpty());
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater.cc b/chromium/chrome/browser/signin/signin_profile_attributes_updater.cc
new file mode 100644
index 00000000000..1f0c6a9ae36
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater.cc
@@ -0,0 +1,72 @@
+// Copyright 2018 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.
+
+#include "chrome/browser/signin/signin_profile_attributes_updater.h"
+
+#include <string>
+
+#include "base/strings/utf_string_conversions.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/signin/signin_util.h"
+#include "chrome/common/pref_names.h"
+#include "components/signin/public/base/consent_level.h"
+#include "components/signin/public/identity_manager/account_info.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+
+SigninProfileAttributesUpdater::SigninProfileAttributesUpdater(
+ signin::IdentityManager* identity_manager,
+ ProfileAttributesStorage* profile_attributes_storage,
+ const base::FilePath& profile_path,
+ PrefService* prefs)
+ : identity_manager_(identity_manager),
+ profile_attributes_storage_(profile_attributes_storage),
+ profile_path_(profile_path),
+ prefs_(prefs) {
+ DCHECK(identity_manager_);
+ DCHECK(profile_attributes_storage_);
+ identity_manager_observation_.Observe(identity_manager_.get());
+
+ UpdateProfileAttributes();
+}
+
+SigninProfileAttributesUpdater::~SigninProfileAttributesUpdater() = default;
+
+void SigninProfileAttributesUpdater::Shutdown() {
+ identity_manager_observation_.Reset();
+}
+
+void SigninProfileAttributesUpdater::UpdateProfileAttributes() {
+ ProfileAttributesEntry* entry =
+ profile_attributes_storage_->GetProfileAttributesWithPath(profile_path_);
+ if (!entry) {
+ return;
+ }
+
+ CoreAccountInfo account_info =
+ identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
+
+ bool clear_profile = account_info.IsEmpty();
+
+ if (account_info.gaia != entry->GetGAIAId() ||
+ !gaia::AreEmailsSame(account_info.email,
+ base::UTF16ToUTF8(entry->GetUserName()))) {
+ // Reset prefs. Note: this will also update the |ProfileAttributesEntry|.
+ prefs_->ClearPref(prefs::kProfileUsingDefaultAvatar);
+ prefs_->ClearPref(prefs::kProfileUsingGAIAAvatar);
+ }
+
+ if (clear_profile) {
+ entry->SetAuthInfo(std::string(), std::u16string(),
+ /*is_consented_primary_account=*/false);
+ } else {
+ entry->SetAuthInfo(
+ account_info.gaia, base::UTF8ToUTF16(account_info.email),
+ identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ }
+}
+
+void SigninProfileAttributesUpdater::OnPrimaryAccountChanged(
+ const signin::PrimaryAccountChangeEvent& event) {
+ UpdateProfileAttributes();
+}
diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater.h b/chromium/chrome/browser/signin/signin_profile_attributes_updater.h
new file mode 100644
index 00000000000..8e189e8e370
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater.h
@@ -0,0 +1,56 @@
+// Copyright 2018 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_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_H_
+
+#include "base/files/file_path.h"
+#include "base/memory/raw_ptr.h"
+#include "base/scoped_observation.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+
+class ProfileAttributesStorage;
+
+// This class listens to various signin events and updates the signin-related
+// fields of ProfileAttributes.
+class SigninProfileAttributesUpdater
+ : public KeyedService,
+ public signin::IdentityManager::Observer {
+ public:
+ SigninProfileAttributesUpdater(
+ signin::IdentityManager* identity_manager,
+ ProfileAttributesStorage* profile_attributes_storage,
+ const base::FilePath& profile_path,
+ PrefService* prefs);
+
+ SigninProfileAttributesUpdater(const SigninProfileAttributesUpdater&) =
+ delete;
+ SigninProfileAttributesUpdater& operator=(
+ const SigninProfileAttributesUpdater&) = delete;
+
+ ~SigninProfileAttributesUpdater() override;
+
+ private:
+ // KeyedService:
+ void Shutdown() override;
+
+ // Updates the profile attributes on signin and signout events.
+ void UpdateProfileAttributes();
+
+ // IdentityManager::Observer:
+ void OnPrimaryAccountChanged(
+ const signin::PrimaryAccountChangeEvent& event) override;
+
+ raw_ptr<signin::IdentityManager> identity_manager_;
+ raw_ptr<ProfileAttributesStorage> profile_attributes_storage_;
+ const base::FilePath profile_path_;
+ raw_ptr<PrefService> prefs_;
+ base::ScopedObservation<signin::IdentityManager,
+ signin::IdentityManager::Observer>
+ identity_manager_observation_{this};
+};
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_H_
diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.cc b/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.cc
new file mode 100644
index 00000000000..86c62a2ce6e
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.cc
@@ -0,0 +1,53 @@
+// Copyright 2018 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.
+
+#include "chrome/browser/signin/signin_profile_attributes_updater_factory.h"
+
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_profile_attributes_updater.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+
+// static
+SigninProfileAttributesUpdater*
+SigninProfileAttributesUpdaterFactory::GetForProfile(Profile* profile) {
+ return static_cast<SigninProfileAttributesUpdater*>(
+ GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+SigninProfileAttributesUpdaterFactory*
+SigninProfileAttributesUpdaterFactory::GetInstance() {
+ return base::Singleton<SigninProfileAttributesUpdaterFactory>::get();
+}
+
+SigninProfileAttributesUpdaterFactory::SigninProfileAttributesUpdaterFactory()
+ : BrowserContextKeyedServiceFactory(
+ "SigninProfileAttributesUpdater",
+ BrowserContextDependencyManager::GetInstance()) {
+ DependsOn(IdentityManagerFactory::GetInstance());
+}
+
+SigninProfileAttributesUpdaterFactory::
+ ~SigninProfileAttributesUpdaterFactory() {}
+
+KeyedService* SigninProfileAttributesUpdaterFactory::BuildServiceInstanceFor(
+ content::BrowserContext* context) const {
+ Profile* profile = Profile::FromBrowserContext(context);
+ // Some tests don't have a ProfileManager, disable this service.
+ if (!g_browser_process->profile_manager())
+ return nullptr;
+
+ return new SigninProfileAttributesUpdater(
+ IdentityManagerFactory::GetForProfile(profile),
+ &g_browser_process->profile_manager()->GetProfileAttributesStorage(),
+ profile->GetPath(), profile->GetPrefs());
+}
+
+bool SigninProfileAttributesUpdaterFactory::ServiceIsCreatedWithBrowserContext()
+ const {
+ return true;
+}
diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.h b/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.h
new file mode 100644
index 00000000000..78f990abaf8
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.h
@@ -0,0 +1,42 @@
+// Copyright 2018 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_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_FACTORY_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_FACTORY_H_
+
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class Profile;
+class SigninProfileAttributesUpdater;
+
+class SigninProfileAttributesUpdaterFactory
+ : public BrowserContextKeyedServiceFactory {
+ public:
+ // Returns nullptr if this profile cannot have a
+ // SigninProfileAttributesUpdater (for example, if |profile| is incognito).
+ static SigninProfileAttributesUpdater* GetForProfile(Profile* profile);
+
+ // Returns an instance of the factory singleton.
+ static SigninProfileAttributesUpdaterFactory* GetInstance();
+
+ SigninProfileAttributesUpdaterFactory(
+ const SigninProfileAttributesUpdaterFactory&) = delete;
+ SigninProfileAttributesUpdaterFactory& operator=(
+ const SigninProfileAttributesUpdaterFactory&) = delete;
+
+ private:
+ friend struct base::DefaultSingletonTraits<
+ SigninProfileAttributesUpdaterFactory>;
+
+ SigninProfileAttributesUpdaterFactory();
+ ~SigninProfileAttributesUpdaterFactory() override;
+
+ // BrowserContextKeyedServiceFactory:
+ KeyedService* BuildServiceInstanceFor(
+ content::BrowserContext* profile) const override;
+ bool ServiceIsCreatedWithBrowserContext() const override;
+};
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_FACTORY_H_
diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater_unittest.cc b/chromium/chrome/browser/signin/signin_profile_attributes_updater_unittest.cc
new file mode 100644
index 00000000000..01430a350b0
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater_unittest.cc
@@ -0,0 +1,224 @@
+// Copyright 2018 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.
+
+#include "chrome/browser/signin/signin_profile_attributes_updater.h"
+
+#include "base/bind.h"
+#include "base/files/file_path.h"
+#include "base/memory/raw_ptr.h"
+#include "base/strings/utf_string_conversions.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/signin/signin_util.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "chrome/test/base/testing_profile.h"
+#include "chrome/test/base/testing_profile_manager.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "components/sync_preferences/pref_service_syncable.h"
+#include "content/public/test/browser_task_environment.h"
+#include "google_apis/gaia/google_service_auth_error.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+const char kEmail[] = "example@email.com";
+
+void CheckProfilePrefsReset(PrefService* pref_service,
+ bool expected_using_default_name) {
+ EXPECT_TRUE(pref_service->GetBoolean(prefs::kProfileUsingDefaultAvatar));
+ EXPECT_FALSE(pref_service->GetBoolean(prefs::kProfileUsingGAIAAvatar));
+ EXPECT_EQ(expected_using_default_name,
+ pref_service->GetBoolean(prefs::kProfileUsingDefaultName));
+}
+
+void CheckProfilePrefsSet(PrefService* pref_service,
+ bool expected_is_using_default_name) {
+ EXPECT_FALSE(pref_service->GetBoolean(prefs::kProfileUsingDefaultAvatar));
+ EXPECT_TRUE(pref_service->GetBoolean(prefs::kProfileUsingGAIAAvatar));
+ EXPECT_EQ(expected_is_using_default_name,
+ pref_service->GetBoolean(prefs::kProfileUsingDefaultName));
+}
+
+// Set the prefs to nondefault values.
+void SetProfilePrefs(PrefService* pref_service) {
+ pref_service->SetBoolean(prefs::kProfileUsingDefaultAvatar, false);
+ pref_service->SetBoolean(prefs::kProfileUsingGAIAAvatar, true);
+ pref_service->SetBoolean(prefs::kProfileUsingDefaultName, false);
+
+ CheckProfilePrefsSet(pref_service, false);
+}
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
+} // namespace
+
+class SigninProfileAttributesUpdaterTest : public testing::Test {
+ public:
+ SigninProfileAttributesUpdaterTest()
+ : profile_manager_(TestingBrowserProcess::GetGlobal()) {}
+
+ // Recreates |signin_profile_attributes_updater_|. Useful for tests that want
+ // to set up the updater with specific preconditions.
+ void RecreateSigninProfileAttributesUpdater() {
+ signin_profile_attributes_updater_ =
+ std::make_unique<SigninProfileAttributesUpdater>(
+ identity_test_env_.identity_manager(),
+ profile_manager_.profile_attributes_storage(), profile_->GetPath(),
+ profile_->GetPrefs());
+ }
+
+ void SetUp() override {
+ testing::Test::SetUp();
+
+ ASSERT_TRUE(profile_manager_.SetUp());
+ std::string name = "profile_name";
+ profile_ = profile_manager_.CreateTestingProfile(
+ name, /*prefs=*/nullptr, base::UTF8ToUTF16(name), 0, std::string(),
+ TestingProfile::TestingFactories());
+
+ RecreateSigninProfileAttributesUpdater();
+ }
+
+ content::BrowserTaskEnvironment task_environment_;
+ TestingProfileManager profile_manager_;
+ raw_ptr<TestingProfile> profile_;
+ signin::IdentityTestEnvironment identity_test_env_;
+ std::unique_ptr<SigninProfileAttributesUpdater>
+ signin_profile_attributes_updater_;
+};
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+// Tests that the browser state info is updated on signin and signout.
+// ChromeOS does not support signout.
+TEST_F(SigninProfileAttributesUpdaterTest, SigninSignout) {
+ ProfileAttributesEntry* entry =
+ profile_manager_.profile_attributes_storage()
+ ->GetProfileAttributesWithPath(profile_->GetPath());
+ ASSERT_NE(entry, nullptr);
+ ASSERT_EQ(entry->GetSigninState(), SigninState::kNotSignedIn);
+ EXPECT_FALSE(entry->IsSigninRequired());
+
+ // Signin.
+ identity_test_env_.MakePrimaryAccountAvailable(kEmail,
+ signin::ConsentLevel::kSync);
+ EXPECT_TRUE(entry->IsAuthenticated());
+ EXPECT_EQ(signin::GetTestGaiaIdForEmail(kEmail), entry->GetGAIAId());
+ EXPECT_EQ(kEmail, base::UTF16ToUTF8(entry->GetUserName()));
+
+ // Signout.
+ identity_test_env_.ClearPrimaryAccount();
+ EXPECT_EQ(entry->GetSigninState(), SigninState::kNotSignedIn);
+ EXPECT_FALSE(entry->IsSigninRequired());
+}
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+TEST_F(SigninProfileAttributesUpdaterTest, SigninSignoutResetsProfilePrefs) {
+ PrefService* pref_service = profile_->GetPrefs();
+ ProfileAttributesEntry* entry =
+ profile_manager_.profile_attributes_storage()
+ ->GetProfileAttributesWithPath(profile_->GetPath());
+ ASSERT_NE(entry, nullptr);
+
+ // Set profile prefs.
+ CheckProfilePrefsReset(pref_service, true);
+#if !defined(OS_ANDROID)
+ SetProfilePrefs(pref_service);
+
+ // Set UPA should reset profile prefs.
+ AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable(
+ "email1@example.com", signin::ConsentLevel::kSignin);
+ EXPECT_FALSE(entry->IsAuthenticated());
+ CheckProfilePrefsReset(pref_service, false);
+ SetProfilePrefs(pref_service);
+ // Signout should reset profile prefs.
+ identity_test_env_.ClearPrimaryAccount();
+ CheckProfilePrefsReset(pref_service, false);
+#endif // !defined(OS_ANDROID)
+
+ SetProfilePrefs(pref_service);
+ // Set primary account should reset profile prefs.
+ AccountInfo primary_account = identity_test_env_.MakePrimaryAccountAvailable(
+ "primary@example.com", signin::ConsentLevel::kSync);
+ CheckProfilePrefsReset(pref_service, false);
+ SetProfilePrefs(pref_service);
+ // Disabling sync should reset profile prefs.
+ identity_test_env_.ClearPrimaryAccount();
+ CheckProfilePrefsReset(pref_service, false);
+}
+
+#if !defined(OS_ANDROID)
+TEST_F(SigninProfileAttributesUpdaterTest,
+ EnablingSyncWithUPAAccountShouldNotResetProfilePrefs) {
+ PrefService* pref_service = profile_->GetPrefs();
+ ProfileAttributesEntry* entry =
+ profile_manager_.profile_attributes_storage()
+ ->GetProfileAttributesWithPath(profile_->GetPath());
+ ASSERT_NE(entry, nullptr);
+ // Set UPA.
+ AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable(
+ "email1@example.com", signin::ConsentLevel::kSignin);
+ EXPECT_FALSE(entry->IsAuthenticated());
+ SetProfilePrefs(pref_service);
+ // Set primary account to be the same as the UPA.
+ // Given it is the same account, profile prefs should keep the same state.
+ identity_test_env_.SetPrimaryAccount(account_info.email,
+ signin::ConsentLevel::kSync);
+ EXPECT_TRUE(entry->IsAuthenticated());
+ CheckProfilePrefsSet(pref_service, false);
+ identity_test_env_.ClearPrimaryAccount();
+ CheckProfilePrefsReset(pref_service, false);
+}
+
+TEST_F(SigninProfileAttributesUpdaterTest,
+ EnablingSyncWithDifferentAccountThanUPAResetsProfilePrefs) {
+ PrefService* pref_service = profile_->GetPrefs();
+ ProfileAttributesEntry* entry =
+ profile_manager_.profile_attributes_storage()
+ ->GetProfileAttributesWithPath(profile_->GetPath());
+ ASSERT_NE(entry, nullptr);
+ AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable(
+ "email1@example.com", signin::ConsentLevel::kSignin);
+ EXPECT_FALSE(entry->IsAuthenticated());
+ SetProfilePrefs(pref_service);
+ // Set primary account to a different account than the UPA.
+ AccountInfo primary_account = identity_test_env_.MakePrimaryAccountAvailable(
+ "primary@example.com", signin::ConsentLevel::kSync);
+ EXPECT_TRUE(entry->IsAuthenticated());
+ CheckProfilePrefsReset(pref_service, false);
+}
+#endif // !defined(OS_ANDROID)
+
+class SigninProfileAttributesUpdaterWithForceSigninTest
+ : public SigninProfileAttributesUpdaterTest {
+ public:
+ SigninProfileAttributesUpdaterWithForceSigninTest()
+ : forced_signin_setter_(true) {}
+
+ private:
+ signin_util::ScopedForceSigninSetterForTesting forced_signin_setter_;
+};
+
+TEST_F(SigninProfileAttributesUpdaterWithForceSigninTest, IsSigninRequired) {
+ ProfileAttributesEntry* entry =
+ profile_manager_.profile_attributes_storage()
+ ->GetProfileAttributesWithPath(profile_->GetPath());
+ ASSERT_NE(entry, nullptr);
+ EXPECT_FALSE(entry->IsAuthenticated());
+ EXPECT_TRUE(entry->IsSigninRequired());
+
+ AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable(
+ kEmail, signin::ConsentLevel::kSync);
+
+ EXPECT_TRUE(entry->IsAuthenticated());
+ EXPECT_EQ(signin::GetTestGaiaIdForEmail(kEmail), entry->GetGAIAId());
+ EXPECT_EQ(kEmail, base::UTF16ToUTF8(entry->GetUserName()));
+
+ identity_test_env_.ClearPrimaryAccount();
+ EXPECT_EQ(entry->GetSigninState(), SigninState::kNotSignedIn);
+ EXPECT_TRUE(entry->IsSigninRequired());
+}
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
diff --git a/chromium/chrome/browser/signin/signin_promo.cc b/chromium/chrome/browser/signin/signin_promo.cc
new file mode 100644
index 00000000000..9315cb8a924
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_promo.cc
@@ -0,0 +1,143 @@
+// Copyright 2013 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.
+
+#include "chrome/browser/signin/signin_promo.h"
+
+#include "base/strings/string_number_conversions.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/google/google_brand.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/signin_promo_util.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/common/url_constants.h"
+#include "components/google/core/common/google_util.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/prefs/pref_service.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/storage_partition_config.h"
+#include "extensions/browser/guest_view/web_view/web_view_guest.h"
+#include "google_apis/gaia/gaia_urls.h"
+#include "net/base/url_util.h"
+#include "url/gurl.h"
+
+#if defined(OS_WIN)
+#include "base/win/windows_version.h"
+#endif
+
+namespace signin {
+
+const char kSignInPromoQueryKeyAccessPoint[] = "access_point";
+const char kSignInPromoQueryKeyAutoClose[] = "auto_close";
+const char kSignInPromoQueryKeyForceKeepData[] = "force_keep_data";
+const char kSignInPromoQueryKeyReason[] = "reason";
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+GURL GetEmbeddedPromoURL(signin_metrics::AccessPoint access_point,
+ signin_metrics::Reason reason,
+ bool auto_close) {
+ CHECK_LT(static_cast<int>(access_point),
+ static_cast<int>(signin_metrics::AccessPoint::ACCESS_POINT_MAX));
+ CHECK_NE(static_cast<int>(access_point),
+ static_cast<int>(signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN));
+ CHECK_LE(static_cast<int>(reason),
+ static_cast<int>(signin_metrics::Reason::kMaxValue));
+ CHECK_NE(static_cast<int>(reason),
+ static_cast<int>(signin_metrics::Reason::kUnknownReason));
+
+ GURL url(chrome::kChromeUIChromeSigninURL);
+ url = net::AppendQueryParameter(
+ url, signin::kSignInPromoQueryKeyAccessPoint,
+ base::NumberToString(static_cast<int>(access_point)));
+ url =
+ net::AppendQueryParameter(url, signin::kSignInPromoQueryKeyReason,
+ base::NumberToString(static_cast<int>(reason)));
+ if (auto_close) {
+ url = net::AppendQueryParameter(url, signin::kSignInPromoQueryKeyAutoClose,
+ "1");
+ }
+ return url;
+}
+
+GURL GetEmbeddedReauthURLWithEmail(signin_metrics::AccessPoint access_point,
+ signin_metrics::Reason reason,
+ const std::string& email) {
+ GURL url = GetEmbeddedPromoURL(access_point, reason, /*auto_close=*/true);
+ url = net::AppendQueryParameter(url, "email", email);
+ url = net::AppendQueryParameter(url, "validateEmail", "1");
+ return net::AppendQueryParameter(url, "readOnlyEmail", "1");
+}
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
+
+GURL GetChromeSyncURLForDice(const std::string& email,
+ const std::string& continue_url) {
+ GURL url = GaiaUrls::GetInstance()->signin_chrome_sync_dice();
+ if (!email.empty())
+ url = net::AppendQueryParameter(url, "email_hint", email);
+ if (!continue_url.empty())
+ url = net::AppendQueryParameter(url, "continue", continue_url);
+ return url;
+}
+
+GURL GetAddAccountURLForDice(const std::string& email,
+ const std::string& continue_url) {
+ GURL url = GaiaUrls::GetInstance()->add_account_url();
+ if (!email.empty())
+ url = net::AppendQueryParameter(url, "Email", email);
+ if (!continue_url.empty())
+ url = net::AppendQueryParameter(url, "continue", continue_url);
+ return url;
+}
+
+content::StoragePartition* GetSigninPartition(
+ content::BrowserContext* browser_context) {
+ const auto signin_partition_config = content::StoragePartitionConfig::Create(
+ browser_context, "chrome-signin", /* partition_name= */ "",
+ /* in_memory= */ true);
+ return browser_context->GetStoragePartition(signin_partition_config);
+}
+
+signin_metrics::AccessPoint GetAccessPointForEmbeddedPromoURL(const GURL& url) {
+ std::string value;
+ if (!net::GetValueForKeyInQuery(url, kSignInPromoQueryKeyAccessPoint,
+ &value)) {
+ return signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN;
+ }
+
+ int access_point = -1;
+ base::StringToInt(value, &access_point);
+ if (access_point <
+ static_cast<int>(
+ signin_metrics::AccessPoint::ACCESS_POINT_START_PAGE) ||
+ access_point >=
+ static_cast<int>(signin_metrics::AccessPoint::ACCESS_POINT_MAX)) {
+ return signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN;
+ }
+
+ return static_cast<signin_metrics::AccessPoint>(access_point);
+}
+
+signin_metrics::Reason GetSigninReasonForEmbeddedPromoURL(const GURL& url) {
+ std::string value;
+ if (!net::GetValueForKeyInQuery(url, kSignInPromoQueryKeyReason, &value))
+ return signin_metrics::Reason::kUnknownReason;
+
+ int reason = -1;
+ base::StringToInt(value, &reason);
+ if (reason <
+ static_cast<int>(signin_metrics::Reason::kSigninPrimaryAccount) ||
+ reason > static_cast<int>(signin_metrics::Reason::kMaxValue)) {
+ return signin_metrics::Reason::kUnknownReason;
+ }
+
+ return static_cast<signin_metrics::Reason>(reason);
+}
+
+void RegisterProfilePrefs(
+ user_prefs::PrefRegistrySyncable* registry) {
+ registry->RegisterIntegerPref(prefs::kDiceSigninUserMenuPromoCount, 0);
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/signin_promo.h b/chromium/chrome/browser/signin/signin_promo.h
new file mode 100644
index 00000000000..36cbc05b7c4
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_promo.h
@@ -0,0 +1,83 @@
+// Copyright 2013 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_SIGNIN_SIGNIN_PROMO_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_H_
+
+#include <string>
+
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "components/signin/public/base/signin_metrics.h"
+
+class GURL;
+
+namespace content {
+class BrowserContext;
+class StoragePartition;
+} // namespace content
+
+namespace user_prefs {
+class PrefRegistrySyncable;
+}
+
+// Utility functions for sign in promos.
+namespace signin {
+
+extern const char kSignInPromoQueryKeyAccessPoint[];
+// TODO(https://crbug.com/1205147): Auto close is unused. Remove it.
+extern const char kSignInPromoQueryKeyAutoClose[];
+extern const char kSignInPromoQueryKeyForceKeepData[];
+extern const char kSignInPromoQueryKeyReason[];
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+// These functions are only used to unlock the profile from the desktop user
+// manager and the windows credential provider.
+
+// Returns the sign in promo URL that can be used in a modal dialog with
+// the given arguments in the query.
+// |access_point| indicates where the sign in is being initiated.
+// |reason| indicates the purpose of using this URL.
+// |auto_close| whether to close the sign in promo automatically when done.
+GURL GetEmbeddedPromoURL(signin_metrics::AccessPoint access_point,
+ signin_metrics::Reason reason,
+ bool auto_close);
+
+// Returns a sign in promo URL specifically for reauthenticating |email| that
+// can be used in a modal dialog.
+GURL GetEmbeddedReauthURLWithEmail(signin_metrics::AccessPoint access_point,
+ signin_metrics::Reason reason,
+ const std::string& email);
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
+
+// Returns the URL to be used to signin and turn on Sync when DICE is enabled.
+// If email is not empty, then it will pass email as hint to the page so that it
+// will be autofilled by Gaia.
+// If |continue_url| is empty, this may redirect to myaccount.
+GURL GetChromeSyncURLForDice(const std::string& email,
+ const std::string& continue_url);
+
+// Returns the URL to be used to add (secondary) account when DICE is enabled.
+// If email is not empty, then it will pass email as hint to the page so that it
+// will be autofilled by Gaia.
+// If |continue_url| is empty, this may redirect to myaccount.
+GURL GetAddAccountURLForDice(const std::string& email,
+ const std::string& continue_url);
+
+// Gets the partition for the embedded sign in frame/webview.
+content::StoragePartition* GetSigninPartition(
+ content::BrowserContext* browser_context);
+
+// Gets the access point from the query portion of the sign in promo URL.
+signin_metrics::AccessPoint GetAccessPointForEmbeddedPromoURL(const GURL& url);
+
+// Gets the sign in reason from the query portion of the sign in promo URL.
+signin_metrics::Reason GetSigninReasonForEmbeddedPromoURL(const GURL& url);
+
+// Registers the preferences the Sign In Promo needs.
+void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_H_
diff --git a/chromium/chrome/browser/signin/signin_promo_unittest.cc b/chromium/chrome/browser/signin/signin_promo_unittest.cc
new file mode 100644
index 00000000000..c6dc4054af2
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_promo_unittest.cc
@@ -0,0 +1,56 @@
+// Copyright 2017 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.
+
+#include "chrome/browser/signin/signin_promo.h"
+
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/common/webui_url_constants.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace signin {
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+TEST(SigninPromoTest, TestPromoURL) {
+ GURL::Replacements replace_query;
+ replace_query.SetQueryStr("access_point=0&reason=0&auto_close=1");
+ EXPECT_EQ(
+ GURL(chrome::kChromeUIChromeSigninURL).ReplaceComponents(replace_query),
+ GetEmbeddedPromoURL(signin_metrics::AccessPoint::ACCESS_POINT_START_PAGE,
+ signin_metrics::Reason::kSigninPrimaryAccount, true));
+ replace_query.SetQueryStr("access_point=15&reason=1");
+ EXPECT_EQ(
+ GURL(chrome::kChromeUIChromeSigninURL).ReplaceComponents(replace_query),
+ GetEmbeddedPromoURL(
+ signin_metrics::AccessPoint::ACCESS_POINT_SIGNIN_PROMO,
+ signin_metrics::Reason::kAddSecondaryAccount, false));
+}
+
+TEST(SigninPromoTest, TestReauthURL) {
+ GURL::Replacements replace_query;
+ replace_query.SetQueryStr(
+ "access_point=0&reason=6&auto_close=1"
+ "&email=example%40domain.com&validateEmail=1"
+ "&readOnlyEmail=1");
+ EXPECT_EQ(
+ GURL(chrome::kChromeUIChromeSigninURL).ReplaceComponents(replace_query),
+ GetEmbeddedReauthURLWithEmail(
+ signin_metrics::AccessPoint::ACCESS_POINT_START_PAGE,
+ signin_metrics::Reason::kFetchLstOnly, "example@domain.com"));
+}
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
+
+TEST(SigninPromoTest, SigninURLForDice) {
+ EXPECT_EQ(
+ "https://accounts.google.com/signin/chrome/sync?ssp=1&"
+ "email_hint=email%40gmail.com&continue=https%3A%2F%2Fcontinue_url%2F",
+ GetChromeSyncURLForDice("email@gmail.com", "https://continue_url/"));
+ EXPECT_EQ(
+ "https://accounts.google.com/AddSession?"
+ "Email=email%40gmail.com&continue=https%3A%2F%2Fcontinue_url%2F",
+ GetAddAccountURLForDice("email@gmail.com", "https://continue_url/"));
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/signin_promo_util.cc b/chromium/chrome/browser/signin/signin_promo_util.cc
new file mode 100644
index 00000000000..4497bbf6b72
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_promo_util.cc
@@ -0,0 +1,49 @@
+// Copyright 2016 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.
+
+#include "chrome/browser/signin/signin_promo_util.h"
+
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/primary_account_mutator.h"
+#include "net/base/network_change_notifier.h"
+
+namespace signin {
+
+bool ShouldShowPromo(Profile* profile) {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ // There's no need to show the sign in promo on cros since cros users are
+ // already logged in.
+ return false;
+#else
+
+ // Don't bother if we don't have any kind of network connection.
+ if (net::NetworkChangeNotifier::IsOffline())
+ return false;
+
+ // Consider original profile even if an off-the-record profile was
+ // passed to this method as sign-in state is only defined for the
+ // primary profile.
+ Profile* original_profile = profile->GetOriginalProfile();
+
+ // Don't show for supervised child profiles.
+ if (original_profile->IsChild())
+ return false;
+
+ // Don't show if sign-in is not allowed.
+ if (!original_profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed))
+ return false;
+
+ // Display the signin promo if the user is not signed in.
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(original_profile);
+ return !identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync);
+#endif
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/signin_promo_util.h b/chromium/chrome/browser/signin/signin_promo_util.h
new file mode 100644
index 00000000000..36a68d39e00
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_promo_util.h
@@ -0,0 +1,18 @@
+// Copyright 2016 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_SIGNIN_SIGNIN_PROMO_UTIL_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_UTIL_H_
+
+class Profile;
+
+namespace signin {
+
+// Returns true if the sign in promo should be visible.
+// |profile| is the profile of the tab the promo would be shown on.
+bool ShouldShowPromo(Profile* profile);
+
+} // namespace signin
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_UTIL_H_
diff --git a/chromium/chrome/browser/signin/signin_ui_util.cc b/chromium/chrome/browser/signin/signin_ui_util.cc
new file mode 100644
index 00000000000..18c541f96a2
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_ui_util.cc
@@ -0,0 +1,608 @@
+// Copyright (c) 2013 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.
+
+#include "chrome/browser/signin/signin_ui_util.h"
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/callback_helpers.h"
+#include "base/feature_list.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/metrics/user_metrics.h"
+#include "base/notreached.h"
+#include "base/strings/strcat.h"
+#include "base/strings/string_util.h"
+#include "base/strings/sys_string_conversions.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/supports_user_data.h"
+#include "base/time/time.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/feature_engagement/tracker_factory.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_navigator.h"
+#include "chrome/browser/ui/browser_navigator_params.h"
+#include "chrome/browser/ui/chrome_pages.h"
+#include "chrome/browser/ui/scoped_tabbed_browser_displayer.h"
+#include "chrome/browser/ui/ui_features.h"
+#include "chrome/common/pref_names.h"
+#include "components/feature_engagement/public/tracker.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/consent_level.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/account_info.h"
+#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_utils.h"
+#include "third_party/re2/src/re2/re2.h"
+#include "ui/gfx/font_list.h"
+#include "ui/gfx/text_elider.h"
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/ash/profiles/profile_helper.h"
+#endif
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+#include "components/account_manager_core/account_manager_facade.h"
+#include "components/account_manager_core/chromeos/account_manager_facade_factory.h"
+#endif
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h"
+#endif
+
+namespace {
+
+// Key for storing animated identity per-profile data.
+const char kAnimatedIdentityKeyName[] = "animated_identity_user_data";
+
+constexpr base::TimeDelta kDelayForCrossWindowAnimationReplay =
+ base::Seconds(5);
+
+// UserData attached to the user profile, keeping track of the last time the
+// animation was shown to the user.
+class AvatarButtonUserData : public base::SupportsUserData::Data {
+ public:
+ ~AvatarButtonUserData() override = default;
+
+ // Returns the last time the animated identity was shown. Returns the null
+ // time if it was never shown.
+ static base::TimeTicks GetAnimatedIdentityLastShown(Profile* profile) {
+ DCHECK(profile);
+ AvatarButtonUserData* data = GetForProfile(profile);
+ if (!data)
+ return base::TimeTicks();
+ return data->animated_identity_last_shown_;
+ }
+
+ // Sets the time when the animated identity was shown.
+ static void SetAnimatedIdentityLastShown(Profile* profile,
+ base::TimeTicks time) {
+ DCHECK(!time.is_null());
+ GetOrCreateForProfile(profile)->animated_identity_last_shown_ = time;
+ }
+
+ private:
+ // Returns nullptr if there is no AvatarButtonUserData attached to the
+ // profile.
+ static AvatarButtonUserData* GetForProfile(Profile* profile) {
+ return static_cast<AvatarButtonUserData*>(
+ profile->GetUserData(kAnimatedIdentityKeyName));
+ }
+
+ // Never returns nullptr.
+ static AvatarButtonUserData* GetOrCreateForProfile(Profile* profile) {
+ DCHECK(profile);
+ AvatarButtonUserData* existing_data = GetForProfile(profile);
+ if (existing_data)
+ return existing_data;
+
+ auto new_data = std::make_unique<AvatarButtonUserData>();
+ auto* new_data_ptr = new_data.get();
+ profile->SetUserData(kAnimatedIdentityKeyName, std::move(new_data));
+ return new_data_ptr;
+ }
+
+ base::TimeTicks animated_identity_last_shown_;
+};
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+void CreateDiceTurnSyncOnHelper(
+ Profile* profile,
+ Browser* browser,
+ signin_metrics::AccessPoint signin_access_point,
+ signin_metrics::PromoAction signin_promo_action,
+ signin_metrics::Reason signin_reason,
+ const CoreAccountId& account_id,
+ DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode) {
+ // DiceTurnSyncOnHelper is suicidal (it will delete itself once it finishes
+ // enabling sync).
+ new DiceTurnSyncOnHelper(profile, browser, signin_access_point,
+ signin_promo_action, signin_reason, account_id,
+ signin_aborted_mode);
+}
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+std::string GetReauthAccessPointHistogramSuffix(
+ signin_metrics::ReauthAccessPoint access_point) {
+ switch (access_point) {
+ case signin_metrics::ReauthAccessPoint::kUnknown:
+ NOTREACHED();
+ return std::string();
+ case signin_metrics::ReauthAccessPoint::kAutofillDropdown:
+ return "ToFillPassword";
+ case signin_metrics::ReauthAccessPoint::kPasswordSaveBubble:
+ return "ToSaveOrUpdatePassword";
+ case signin_metrics::ReauthAccessPoint::kPasswordSettings:
+ return "ToManageInSettings";
+ case signin_metrics::ReauthAccessPoint::kGeneratePasswordDropdown:
+ case signin_metrics::ReauthAccessPoint::kGeneratePasswordContextMenu:
+ return "ToGeneratePassword";
+ case signin_metrics::ReauthAccessPoint::kPasswordMoveBubble:
+ return "ToMovePassword";
+ case signin_metrics::ReauthAccessPoint::kPasswordSaveLocallyBubble:
+ return "ToSavePasswordLocallyThenMove";
+ }
+}
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+account_manager::AccountManagerFacade::AccountAdditionSource
+GetAccountReauthSourceFromAccessPoint(
+ signin_metrics::AccessPoint access_point) {
+ switch (access_point) {
+ case signin_metrics::AccessPoint::ACCESS_POINT_AVATAR_BUBBLE_SIGN_IN:
+ return account_manager::AccountManagerFacade::AccountAdditionSource::
+ kAvatarBubbleReauthAccountButton;
+ default:
+ NOTREACHED() << "Reauth is requested from an unknown access point "
+ << static_cast<int>(access_point);
+ return account_manager::AccountManagerFacade::AccountAdditionSource::
+ kMaxValue;
+ }
+}
+#endif
+
+} // namespace
+
+namespace signin_ui_util {
+
+std::u16string GetAuthenticatedUsername(Profile* profile) {
+ DCHECK(profile);
+ std::string user_display_name;
+ auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
+ if (identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) {
+ user_display_name =
+ identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSync)
+ .email;
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ // See https://crbug.com/994798 for details.
+ user_manager::User* user =
+ chromeos::ProfileHelper::Get()->GetUserByProfile(profile);
+ // |user| may be null in tests.
+ if (user)
+ user_display_name = user->GetDisplayEmail();
+#endif // BUILDFLAG(IS_CHROMEOS_ASH)
+ }
+
+ return base::UTF8ToUTF16(user_display_name);
+}
+
+void InitializePrefsForProfile(Profile* profile) {
+ if (profile->IsNewProfile()) {
+ // Suppresses the upgrade tutorial for a new profile.
+ profile->GetPrefs()->SetInteger(prefs::kProfileAvatarTutorialShown,
+ kUpgradeWelcomeTutorialShowMax + 1);
+ }
+}
+
+void ShowSigninErrorLearnMorePage(Profile* profile) {
+ static const char kSigninErrorLearnMoreUrl[] =
+ "https://support.google.com/chrome/answer/1181420?";
+ NavigateParams params(profile, GURL(kSigninErrorLearnMoreUrl),
+ ui::PAGE_TRANSITION_LINK);
+ params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
+ Navigate(&params);
+}
+
+void ShowReauthForPrimaryAccountWithAuthError(
+ Browser* browser,
+ signin_metrics::AccessPoint access_point) {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ // On ChromeOS, sync errors are fixed by re-signing into the OS.
+ NOTREACHED();
+#elif BUILDFLAG(IS_CHROMEOS_LACROS)
+ internal::ShowReauthForPrimaryAccountWithAuthErrorLacros(
+ browser, access_point,
+ ::GetAccountManagerFacade(browser->profile()->GetPath().value()));
+#else
+ browser->signin_view_controller()->ShowSignin(
+ profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH, access_point);
+#endif
+}
+
+void ShowExtensionSigninPrompt(Profile* profile,
+ bool enable_sync,
+ const std::string& email_hint) {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+ NOTREACHED();
+#else
+ internal::ShowExtensionSigninPrompt(
+ profile,
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+ ::GetAccountManagerFacade(profile->GetPath().value()),
+#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
+ enable_sync, email_hint);
+#endif // BUILDFLAG(IS_CHROMEOS_ASH)
+}
+
+namespace internal {
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+void ShowReauthForPrimaryAccountWithAuthErrorLacros(
+ Browser* browser,
+ signin_metrics::AccessPoint access_point,
+ account_manager::AccountManagerFacade* account_manager_facade) {
+ Profile* profile = browser->profile();
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ CoreAccountInfo primary_account_info =
+ identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
+ DCHECK(!primary_account_info.IsEmpty());
+ DCHECK(identity_manager->HasAccountWithRefreshTokenInPersistentErrorState(
+ primary_account_info.account_id));
+ account_manager_facade->ShowReauthAccountDialog(
+ GetAccountReauthSourceFromAccessPoint(access_point),
+ primary_account_info.email);
+}
+#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+void ShowExtensionSigninPrompt(
+ Profile* profile,
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+ account_manager::AccountManagerFacade* account_manager_facade,
+#endif
+ bool enable_sync,
+ const std::string& email_hint) {
+ // There is no sign-in flow for guest or system profile.
+ if (profile->IsGuestSession() || profile->IsSystemProfile())
+ return;
+ // Locked profile should be unlocked with UserManager only.
+ ProfileAttributesEntry* entry =
+ g_browser_process->profile_manager()
+ ->GetProfileAttributesStorage()
+ .GetProfileAttributesWithPath(profile->GetPath());
+ if (entry && entry->IsSigninRequired()) {
+ return;
+ }
+
+ // This may be called in incognito. Redirect to the original profile.
+ profile = profile->GetOriginalProfile();
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+ // There is no sign-in without Mirror.
+ if (!AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile))
+ return;
+
+ if (email_hint.empty()) {
+ // Add a new account.
+ // TODO(https://crbug.com/1260291): add support for signed out profiles.
+ NOTREACHED() << "Lacros doesn't support signed-out profiles yet.";
+ return;
+ }
+
+ // Re-authenticate an existing account.
+ account_manager_facade->ShowReauthAccountDialog(
+ account_manager::AccountManagerFacade::AccountAdditionSource::
+ kChromeExtensionReauth,
+ email_hint);
+#elif BUILDFLAG(ENABLE_DICE_SUPPORT)
+ chrome::ScopedTabbedBrowserDisplayer displayer(profile);
+ Browser* browser = displayer.browser();
+
+ // Cannot sign in if browser cannot be displayed.
+ if (!browser)
+ return;
+
+ if (enable_sync) {
+ // Set a primary account.
+ browser->signin_view_controller()->ShowDiceEnableSyncTab(
+ signin_metrics::AccessPoint::ACCESS_POINT_EXTENSIONS,
+ signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO, email_hint);
+ } else {
+ // Add an account to the web without setting a primary account.
+ browser->signin_view_controller()->ShowDiceAddAccountTab(
+ signin_metrics::AccessPoint::ACCESS_POINT_EXTENSIONS, email_hint);
+ }
+#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
+}
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
+
+} // namespace internal
+
+void EnableSyncFromSingleAccountPromo(
+ Browser* browser,
+ const AccountInfo& account,
+ signin_metrics::AccessPoint access_point) {
+ EnableSyncFromMultiAccountPromo(browser, account, access_point,
+ /*is_default_promo_account=*/true);
+}
+
+void EnableSyncFromMultiAccountPromo(Browser* browser,
+ const AccountInfo& account,
+ signin_metrics::AccessPoint access_point,
+ bool is_default_promo_account) {
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+ internal::EnableSyncFromPromo(browser, account, access_point,
+ is_default_promo_account,
+ base::BindOnce(&CreateDiceTurnSyncOnHelper));
+#else
+ NOTREACHED();
+#endif
+}
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+namespace internal {
+void EnableSyncFromPromo(
+ Browser* browser,
+ const AccountInfo& account,
+ signin_metrics::AccessPoint access_point,
+ bool is_default_promo_account,
+ base::OnceCallback<
+ void(Profile* profile,
+ Browser* browser,
+ signin_metrics::AccessPoint signin_access_point,
+ signin_metrics::PromoAction signin_promo_action,
+ signin_metrics::Reason signin_reason,
+ const CoreAccountId& account_id,
+ DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode)>
+ create_dice_turn_sync_on_helper_callback) {
+ DCHECK(browser);
+ DCHECK_NE(signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN, access_point);
+ Profile* profile = browser->profile();
+ DCHECK(!profile->IsOffTheRecord());
+
+ if (IdentityManagerFactory::GetForProfile(profile)->HasPrimaryAccount(
+ signin::ConsentLevel::kSync)) {
+ DVLOG(1) << "There is already a primary account.";
+ return;
+ }
+
+ if (account.IsEmpty()) {
+ chrome::ShowBrowserSignin(browser, access_point,
+ signin::ConsentLevel::kSync);
+ return;
+ }
+
+ DCHECK(!account.account_id.empty());
+ DCHECK(!account.email.empty());
+ DCHECK(AccountConsistencyModeManager::IsDiceEnabledForProfile(profile));
+
+ signin_metrics::PromoAction promo_action =
+ is_default_promo_account
+ ? signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT
+ : signin_metrics::PromoAction::PROMO_ACTION_NOT_DEFAULT;
+
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ bool needs_reauth_before_enable_sync =
+ !identity_manager->HasAccountWithRefreshToken(account.account_id) ||
+ identity_manager->HasAccountWithRefreshTokenInPersistentErrorState(
+ account.account_id);
+ if (needs_reauth_before_enable_sync) {
+ browser->signin_view_controller()->ShowDiceEnableSyncTab(
+ access_point, promo_action, account.email);
+ return;
+ }
+
+ signin_metrics::LogSigninAccessPointStarted(access_point, promo_action);
+ signin_metrics::RecordSigninUserActionForAccessPoint(access_point,
+ promo_action);
+ std::move(create_dice_turn_sync_on_helper_callback)
+ .Run(profile, browser, access_point, promo_action,
+ signin_metrics::Reason::kSigninPrimaryAccount, account.account_id,
+ DiceTurnSyncOnHelper::SigninAbortedMode::KEEP_ACCOUNT);
+}
+} // namespace internal
+
+std::vector<AccountInfo> GetAccountsForDicePromos(Profile* profile) {
+ // Fetch account ids for accounts that have a token.
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ std::vector<AccountInfo> accounts_with_tokens =
+ identity_manager->GetExtendedAccountInfoForAccountsWithRefreshToken();
+
+ // Compute the default account.
+ CoreAccountId default_account_id =
+ identity_manager->GetPrimaryAccountId(signin::ConsentLevel::kSignin);
+
+ // Fetch account information for each id and make sure that the first account
+ // in the list matches the unconsented primary account (if available).
+ std::vector<AccountInfo> accounts;
+ for (auto& account_info : accounts_with_tokens) {
+ DCHECK(!account_info.IsEmpty());
+ if (!signin::IsUsernameAllowedByPatternFromPrefs(
+ g_browser_process->local_state(), account_info.email)) {
+ continue;
+ }
+ if (account_info.account_id == default_account_id)
+ accounts.insert(accounts.begin(), std::move(account_info));
+ else
+ accounts.push_back(std::move(account_info));
+ }
+ return accounts;
+}
+
+AccountInfo GetSingleAccountForDicePromos(Profile* profile) {
+ std::vector<AccountInfo> accounts = GetAccountsForDicePromos(profile);
+ if (!accounts.empty())
+ return accounts[0];
+ return AccountInfo();
+}
+
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+std::u16string GetShortProfileIdentityToDisplay(
+ const ProfileAttributesEntry& profile_attributes_entry,
+ Profile* profile) {
+ DCHECK(profile);
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ CoreAccountInfo core_info =
+ identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
+ // If there's no unconsented primary account, simply return the name of the
+ // profile according to profile attributes.
+ if (core_info.IsEmpty())
+ return profile_attributes_entry.GetName();
+
+ AccountInfo extended_info =
+ identity_manager->FindExtendedAccountInfoByAccountId(
+ core_info.account_id);
+ // If there's no given name available, return the user email.
+ if (extended_info.given_name.empty())
+ return base::UTF8ToUTF16(core_info.email);
+
+ return base::UTF8ToUTF16(extended_info.given_name);
+}
+
+std::string GetAllowedDomain(std::string signin_pattern) {
+ std::vector<std::string> splitted_signin_pattern = base::SplitString(
+ signin_pattern, "@", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
+
+ // There are more than one '@'s in the pattern.
+ if (splitted_signin_pattern.size() != 2)
+ return std::string();
+
+ std::string domain = splitted_signin_pattern[1];
+
+ // Trims tailing '$' if existed.
+ if (!domain.empty() && domain.back() == '$')
+ domain.pop_back();
+
+ // Trims tailing '\E' if existed.
+ if (domain.size() > 1 &&
+ base::EndsWith(domain, "\\E", base::CompareCase::SENSITIVE))
+ domain.erase(domain.size() - 2);
+
+ // Check if there is any special character in the domain. Note that
+ // jsmith@[192.168.2.1] is not supported.
+ if (!re2::RE2::FullMatch(domain, "[a-zA-Z0-9\\-.]+"))
+ return std::string();
+
+ return domain;
+}
+
+bool ShouldShowAnimatedIdentityOnOpeningWindow(
+ const ProfileAttributesStorage& profile_attributes_storage,
+ Profile* profile) {
+ DCHECK(profile);
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ DCHECK(identity_manager->AreRefreshTokensLoaded());
+
+ base::TimeTicks animation_last_shown =
+ AvatarButtonUserData::GetAnimatedIdentityLastShown(profile);
+ // When a new window is created, only show the animation if it was never shown
+ // for this profile, or if it was shown in another window in the last few
+ // seconds (because the user may have missed it).
+ if (!animation_last_shown.is_null() &&
+ base::TimeTicks::Now() - animation_last_shown >
+ kDelayForCrossWindowAnimationReplay) {
+ return false;
+ }
+
+ // Show the user identity for users with multiple profiles.
+ if (profile_attributes_storage.GetNumberOfProfiles() > 1) {
+ return true;
+ }
+
+ // Show the user identity for users with multiple signed-in accounts.
+ return identity_manager->GetAccountsWithRefreshTokens().size() > 1;
+}
+
+void RecordAnimatedIdentityTriggered(Profile* profile) {
+ AvatarButtonUserData::SetAnimatedIdentityLastShown(profile,
+ base::TimeTicks::Now());
+}
+
+void RecordAvatarIconHighlighted(Profile* profile) {
+ base::RecordAction(base::UserMetricsAction("AvatarToolbarButtonHighlighted"));
+}
+
+void RecordProfileMenuViewShown(Profile* profile) {
+ base::RecordAction(base::UserMetricsAction("ProfileMenu_Opened"));
+ if (profile->IsRegularProfile()) {
+ base::RecordAction(base::UserMetricsAction("ProfileMenu_Opened_Regular"));
+ // Record usage for profile switch promo.
+ feature_engagement::TrackerFactory::GetForBrowserContext(profile)
+ ->NotifyEvent("profile_menu_shown");
+ } else if (profile->IsGuestSession()) {
+ base::RecordAction(base::UserMetricsAction("ProfileMenu_Opened_Guest"));
+ } else if (profile->IsIncognitoProfile()) {
+ base::RecordAction(base::UserMetricsAction("ProfileMenu_Opened_Incognito"));
+ }
+
+ base::TimeTicks last_shown =
+ AvatarButtonUserData::GetAnimatedIdentityLastShown(profile);
+ if (!last_shown.is_null()) {
+ base::UmaHistogramLongTimes("Profile.Menu.OpenedAfterAvatarAnimation",
+ base::TimeTicks::Now() - last_shown);
+ }
+}
+
+void RecordProfileMenuClick(Profile* profile) {
+ base::RecordAction(
+ base::UserMetricsAction("ProfileMenu_ActionableItemClicked"));
+ if (profile->IsRegularProfile()) {
+ base::RecordAction(
+ base::UserMetricsAction("ProfileMenu_ActionableItemClicked_Regular"));
+ } else if (profile->IsGuestSession()) {
+ base::RecordAction(
+ base::UserMetricsAction("ProfileMenu_ActionableItemClicked_Guest"));
+ } else if (profile->IsIncognitoProfile()) {
+ base::RecordAction(
+ base::UserMetricsAction("ProfileMenu_ActionableItemClicked_Incognito"));
+ }
+}
+
+void RecordTransactionalReauthResult(
+ signin_metrics::ReauthAccessPoint access_point,
+ signin::ReauthResult result) {
+ const char kHistogramName[] = "Signin.TransactionalReauthResult";
+ base::UmaHistogramEnumeration(kHistogramName, result);
+
+ std::string access_point_suffix =
+ GetReauthAccessPointHistogramSuffix(access_point);
+ if (!access_point_suffix.empty()) {
+ std::string suffixed_histogram_name =
+ base::StrCat({kHistogramName, ".", access_point_suffix});
+ base::UmaHistogramEnumeration(suffixed_histogram_name, result);
+ }
+}
+
+void RecordTransactionalReauthUserAction(
+ signin_metrics::ReauthAccessPoint access_point,
+ SigninReauthViewController::UserAction user_action) {
+ const char kHistogramName[] = "Signin.TransactionalReauthUserAction";
+ base::UmaHistogramEnumeration(kHistogramName, user_action);
+
+ std::string access_point_suffix =
+ GetReauthAccessPointHistogramSuffix(access_point);
+ if (!access_point_suffix.empty()) {
+ std::string suffixed_histogram_name =
+ base::StrCat({kHistogramName, ".", access_point_suffix});
+ base::UmaHistogramEnumeration(suffixed_histogram_name, user_action);
+ }
+}
+
+} // namespace signin_ui_util
diff --git a/chromium/chrome/browser/signin/signin_ui_util.h b/chromium/chrome/browser/signin/signin_ui_util.h
new file mode 100644
index 00000000000..075952e959b
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_ui_util.h
@@ -0,0 +1,188 @@
+// Copyright (c) 2013 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_SIGNIN_SIGNIN_UI_UTIL_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_H_
+
+#include <string>
+#include <vector>
+
+#include "base/callback_forward.h"
+#include "build/buildflag.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/signin/reauth_result.h"
+#include "chrome/browser/ui/signin_reauth_view_controller.h"
+#include "components/signin/public/base/signin_buildflags.h"
+#include "components/signin/public/base/signin_metrics.h"
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h"
+#endif
+
+struct AccountInfo;
+class Browser;
+class Profile;
+class ProfileAttributesEntry;
+class ProfileAttributesStorage;
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+namespace account_manager {
+class AccountManagerFacade;
+}
+#endif
+
+// Utility functions to gather status information from the various signed in
+// services and construct messages suitable for showing in UI.
+namespace signin_ui_util {
+
+// The maximum number of times to show the welcome tutorial for an upgrade user.
+const int kUpgradeWelcomeTutorialShowMax = 1;
+
+// Returns the username of the primary account or an empty string if there is
+// no primary account or the account has not consented to browser sync.
+std::u16string GetAuthenticatedUsername(Profile* profile);
+
+// Initializes signin-related preferences.
+void InitializePrefsForProfile(Profile* profile);
+
+// Shows a learn more page for signin errors.
+void ShowSigninErrorLearnMorePage(Profile* profile);
+
+// Shows a reauth page/dialog to reauthanticate a primary account in error
+// state.
+void ShowReauthForPrimaryAccountWithAuthError(
+ Browser* browser,
+ signin_metrics::AccessPoint access_point);
+
+// Delegates to an existing sign-in tab if one exists. If not, a new sign-in tab
+// is created.
+void ShowExtensionSigninPrompt(Profile* profile,
+ bool enable_sync,
+ const std::string& email_hint);
+
+namespace internal {
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+// Same as `ShowReauthForPrimaryAccountWithAuthError` but with a getter function
+// for AccountManagerFacade so that it can be unit tested.
+void ShowReauthForPrimaryAccountWithAuthErrorLacros(
+ Browser* browser,
+ signin_metrics::AccessPoint access_point,
+ account_manager::AccountManagerFacade* account_manager_facade);
+#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+void ShowExtensionSigninPrompt(
+ Profile* profile,
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+ account_manager::AccountManagerFacade* account_manager_facade,
+#endif
+ bool enable_sync,
+ const std::string& email_hint);
+#endif
+} // namespace internal
+
+// This function is used to enable sync for a given account:
+// * This function does nothing if the user is already signed in to Chrome.
+// * If |account| is empty, then it presents the Chrome sign-in page.
+// * If token service has an invalid refreh token for account |account|,
+// then it presents the Chrome sign-in page with |account.emil| prefilled.
+// * If token service has a valid refresh token for |account|, then it
+// enables sync for |account|.
+void EnableSyncFromSingleAccountPromo(Browser* browser,
+ const AccountInfo& account,
+ signin_metrics::AccessPoint access_point);
+
+// This function is used to enable sync for a given account. It has the same
+// behavior as |EnableSyncFromSingleAccountPromo()| except that it also logs
+// some additional information if the action is started from a promo that
+// supports selecting the account that may be used for sync.
+//
+// |is_default_promo_account| is true if |account| corresponds to the default
+// account in the promo. It is ignored if |account| is empty.
+void EnableSyncFromMultiAccountPromo(Browser* browser,
+ const AccountInfo& account,
+ signin_metrics::AccessPoint access_point,
+ bool is_default_promo_account);
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+// Returns the list of all accounts that have a token. The unconsented primary
+// account will be the first account in the list.
+std::vector<AccountInfo> GetAccountsForDicePromos(Profile* profile);
+
+// Returns single account to use in Dice promos.
+AccountInfo GetSingleAccountForDicePromos(Profile* profile);
+
+#endif
+
+// Returns the short user identity to display for |profile|. It is based on the
+// current unconsented primary account (if exists).
+// TODO(crbug.com/1012179): Move this logic into ProfileAttributesEntry once
+// AvatarToolbarButton becomes an observer of ProfileAttributesStorage and thus
+// ProfileAttributesEntry is up-to-date when AvatarToolbarButton needs it.
+std::u16string GetShortProfileIdentityToDisplay(
+ const ProfileAttributesEntry& profile_attributes_entry,
+ Profile* profile);
+
+// Returns the domain of the policy value of RestrictSigninToPattern. Returns
+// an empty string if the policy is not set or can not be parsed. The parser
+// only supports the policy value that matches [^@]+@[a-zA-Z0-9\-.]+(\\E)?\$?$.
+// Also, the parser does not validate the policy value.
+std::string GetAllowedDomain(std::string signin_pattern);
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+namespace internal {
+// Same as |EnableSyncFromPromo| but with a callback that creates a
+// DiceTurnSyncOnHelper so that it can be unit tested.
+void EnableSyncFromPromo(
+ Browser* browser,
+ const AccountInfo& account,
+ signin_metrics::AccessPoint access_point,
+ bool is_default_promo_account,
+ base::OnceCallback<
+ void(Profile* profile,
+ Browser* browser,
+ signin_metrics::AccessPoint signin_access_point,
+ signin_metrics::PromoAction signin_promo_action,
+ signin_metrics::Reason signin_reason,
+ const CoreAccountId& account_id,
+ DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode)>
+ create_dice_turn_sync_on_helper_callback);
+} // namespace internal
+#endif
+
+// Returns whether Chrome should show the identity of the user (using a brief
+// animation) on opening a new window. IdentityManager's refresh tokens must be
+// loaded when this function gets called.
+bool ShouldShowAnimatedIdentityOnOpeningWindow(
+ const ProfileAttributesStorage& profile_attributes_storage,
+ Profile* profile);
+
+// Records that the animated identity was shown for the given profile. This is
+// used for metrics and to decide whether/when the animation can be shown again.
+void RecordAnimatedIdentityTriggered(Profile* profile);
+
+// Records that the avatar icon was highlighted for the given profile. This is
+// used for metrics.
+void RecordAvatarIconHighlighted(Profile* profile);
+
+// Called when the ProfileMenuView is opened. Used for metrics.
+void RecordProfileMenuViewShown(Profile* profile);
+
+// Called when a button/link in the profile menu was clicked.
+void RecordProfileMenuClick(Profile* profile);
+
+// Records the result of a re-auth challenge to finish a transaction (like
+// unlocking the account store for passwords).
+void RecordTransactionalReauthResult(
+ signin_metrics::ReauthAccessPoint access_point,
+ signin::ReauthResult result);
+
+// Records user action performed in a transactional reauth dialog/tab.
+void RecordTransactionalReauthUserAction(
+ signin_metrics::ReauthAccessPoint access_point,
+ SigninReauthViewController::UserAction user_action);
+
+} // namespace signin_ui_util
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_H_
diff --git a/chromium/chrome/browser/signin/signin_ui_util_browsertest.cc b/chromium/chrome/browser/signin/signin_ui_util_browsertest.cc
new file mode 100644
index 00000000000..299e45539d6
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_ui_util_browsertest.cc
@@ -0,0 +1,85 @@
+// Copyright 2021 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_SIGNIN_SIGNIN_UI_UTIL_BROWSERTEST_CC_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_BROWSERTEST_CC_
+
+#include "chrome/browser/signin/signin_ui_util.h"
+
+#include "base/callback_helpers.h"
+#include "base/test/bind.h"
+#include "build/buildflag.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/signin/public/base/signin_buildflags.h"
+#include "content/public/test/browser_test.h"
+
+#if !BUILDFLAG(ENABLE_DICE_SUPPORT)
+#error This file only contains DICE browser tests for now.
+#endif
+
+namespace signin_ui_util {
+
+class DiceSigninUiUtilBrowserTest : public InProcessBrowserTest {
+ public:
+ DiceSigninUiUtilBrowserTest() = default;
+ ~DiceSigninUiUtilBrowserTest() override = default;
+
+ Profile* CreateProfile() {
+ Profile* new_profile = nullptr;
+ base::RunLoop run_loop;
+ ProfileManager::CreateMultiProfileAsync(
+ u"test_profile", /*icon_index=*/0, /*is_hidden=*/false,
+ base::BindLambdaForTesting(
+ [&new_profile, &run_loop](Profile* profile,
+ Profile::CreateStatus status) {
+ ASSERT_NE(status, Profile::CREATE_STATUS_LOCAL_FAIL);
+ if (status == Profile::CREATE_STATUS_INITIALIZED) {
+ new_profile = profile;
+ run_loop.Quit();
+ }
+ }));
+ run_loop.Run();
+ return new_profile;
+ }
+
+ private:
+};
+
+// Tests that `ShowExtensionSigninPrompt()` doesn't crash when it cannot create
+// a new browser. Regression test for https://crbug.com/1273370.
+IN_PROC_BROWSER_TEST_F(DiceSigninUiUtilBrowserTest,
+ ShowExtensionSigninPrompt_NoBrowser) {
+ Profile* new_profile = CreateProfile();
+
+ // New profile should not have any browser windows.
+ EXPECT_FALSE(chrome::FindBrowserWithProfile(new_profile));
+
+ ShowExtensionSigninPrompt(new_profile, /*enable_sync=*/true,
+ /*email_hint=*/std::string());
+ // `ShowExtensionSigninPrompt()` creates a new browser.
+ Browser* browser = chrome::FindBrowserWithProfile(new_profile);
+ ASSERT_TRUE(browser);
+ EXPECT_EQ(1, browser->tab_strip_model()->count());
+
+ // Profile deletion closes the browser.
+ g_browser_process->profile_manager()->ScheduleProfileForDeletion(
+ new_profile->GetPath(), base::DoNothing());
+ ui_test_utils::WaitForBrowserToClose(browser);
+ EXPECT_FALSE(chrome::FindBrowserWithProfile(new_profile));
+
+ // `ShowExtensionSigninPrompt()` does nothing for deleted profile.
+ ShowExtensionSigninPrompt(new_profile, /*enable_sync=*/true,
+ /*email_hint=*/std::string());
+ EXPECT_FALSE(chrome::FindBrowserWithProfile(new_profile));
+}
+
+} // namespace signin_ui_util
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_BROWSERTEST_CC_
diff --git a/chromium/chrome/browser/signin/signin_ui_util_unittest.cc b/chromium/chrome/browser/signin/signin_ui_util_unittest.cc
new file mode 100644
index 00000000000..43be4319fcc
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_ui_util_unittest.cc
@@ -0,0 +1,736 @@
+// Copyright 2018 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.
+
+#include "chrome/browser/signin/signin_ui_util.h"
+
+#include "base/bind.h"
+#include "base/memory/raw_ptr.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/metrics/user_action_tester.h"
+#include "base/test/task_environment.h"
+#include "build/build_config.h"
+#include "build/buildflag.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/profiles/profile_attributes_init_params.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
+#include "chrome/browser/signin/signin_promo.h"
+#include "chrome/browser/signin/signin_util.h"
+#include "chrome/browser/ui/ui_features.h"
+#include "chrome/test/base/browser_with_test_window_test.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "chrome/test/base/testing_profile_manager.h"
+#include "components/account_id/account_id.h"
+#include "components/google/core/common/google_util.h"
+#include "components/signin/public/base/consent_level.h"
+#include "components/signin/public/base/signin_buildflags.h"
+#include "components/signin/public/identity_manager/account_info.h"
+#include "components/signin/public/identity_manager/accounts_mutator.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "google_apis/gaia/gaia_urls.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+#include "components/account_manager_core/mock_account_manager_facade.h"
+#endif
+
+namespace signin_ui_util {
+
+namespace {
+const char kMainEmail[] = "main_email@example.com";
+const char kMainGaiaID[] = "main_gaia_id";
+const char kSecondaryEmail[] = "secondary_email@example.com";
+const char kSecondaryGaiaID[] = "secondary_gaia_id";
+} // namespace
+
+class GetAllowedDomainTest : public ::testing::Test {};
+
+TEST_F(GetAllowedDomainTest, WithInvalidPattern) {
+ EXPECT_EQ(std::string(), GetAllowedDomain("email"));
+ EXPECT_EQ(std::string(), GetAllowedDomain("email@a@b"));
+ EXPECT_EQ(std::string(), GetAllowedDomain("email@a[b"));
+ EXPECT_EQ(std::string(), GetAllowedDomain("@$"));
+ EXPECT_EQ(std::string(), GetAllowedDomain("@\\E$"));
+ EXPECT_EQ(std::string(), GetAllowedDomain("@\\E$a"));
+ EXPECT_EQ(std::string(), GetAllowedDomain("email@"));
+ EXPECT_EQ(std::string(), GetAllowedDomain("@"));
+ EXPECT_EQ(std::string(), GetAllowedDomain("example@a.com|example@b.com"));
+ EXPECT_EQ(std::string(), GetAllowedDomain(""));
+}
+
+TEST_F(GetAllowedDomainTest, WithValidPattern) {
+ EXPECT_EQ("example.com", GetAllowedDomain("email@example.com"));
+ EXPECT_EQ("example.com", GetAllowedDomain("email@example.com\\E"));
+ EXPECT_EQ("example.com", GetAllowedDomain("email@example.com$"));
+ EXPECT_EQ("example.com", GetAllowedDomain("email@example.com\\E$"));
+ EXPECT_EQ("example.com", GetAllowedDomain("*@example.com\\E$"));
+ EXPECT_EQ("example.com", GetAllowedDomain(".*@example.com\\E$"));
+ EXPECT_EQ("example-1.com", GetAllowedDomain("email@example-1.com"));
+}
+
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+namespace {
+
+class SigninUiUtilTestBrowserWindow : public TestBrowserWindow {
+ public:
+ SigninUiUtilTestBrowserWindow() = default;
+
+ SigninUiUtilTestBrowserWindow(const SigninUiUtilTestBrowserWindow&) = delete;
+ SigninUiUtilTestBrowserWindow& operator=(
+ const SigninUiUtilTestBrowserWindow&) = delete;
+
+ ~SigninUiUtilTestBrowserWindow() override = default;
+ void set_browser(Browser* browser) { browser_ = browser; }
+
+ void ShowAvatarBubbleFromAvatarButton(
+ AvatarBubbleMode mode,
+ signin_metrics::AccessPoint access_point,
+ bool is_source_keyboard) override {
+ ASSERT_TRUE(browser_);
+ // Simulate what |BrowserView| does for a regular Chrome sign-in flow.
+ browser_->signin_view_controller()->ShowSignin(
+ profiles::BubbleViewMode::BUBBLE_VIEW_MODE_GAIA_SIGNIN, access_point);
+ }
+
+ private:
+ raw_ptr<Browser> browser_ = nullptr;
+};
+
+} // namespace
+
+class DiceSigninUiUtilTest : public BrowserWithTestWindowTest {
+ public:
+ DiceSigninUiUtilTest() = default;
+ ~DiceSigninUiUtilTest() override = default;
+
+ struct CreateDiceTurnSyncOnHelperParams {
+ public:
+ raw_ptr<Profile> profile = nullptr;
+ raw_ptr<Browser> browser = nullptr;
+ signin_metrics::AccessPoint signin_access_point =
+ signin_metrics::AccessPoint::ACCESS_POINT_MAX;
+ signin_metrics::PromoAction signin_promo_action =
+ signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO;
+ signin_metrics::Reason signin_reason =
+ signin_metrics::Reason::kUnknownReason;
+ CoreAccountId account_id;
+ DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode =
+ DiceTurnSyncOnHelper::SigninAbortedMode::REMOVE_ACCOUNT;
+ };
+
+ void CreateDiceTurnSyncOnHelper(
+ Profile* profile,
+ Browser* browser,
+ signin_metrics::AccessPoint signin_access_point,
+ signin_metrics::PromoAction signin_promo_action,
+ signin_metrics::Reason signin_reason,
+ const CoreAccountId& account_id,
+ DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode) {
+ create_dice_turn_sync_on_helper_called_ = true;
+ create_dice_turn_sync_on_helper_params_.profile = profile;
+ create_dice_turn_sync_on_helper_params_.browser = browser;
+ create_dice_turn_sync_on_helper_params_.signin_access_point =
+ signin_access_point;
+ create_dice_turn_sync_on_helper_params_.signin_promo_action =
+ signin_promo_action;
+ create_dice_turn_sync_on_helper_params_.signin_reason = signin_reason;
+ create_dice_turn_sync_on_helper_params_.account_id = account_id;
+ create_dice_turn_sync_on_helper_params_.signin_aborted_mode =
+ signin_aborted_mode;
+ }
+
+ protected:
+ // BrowserWithTestWindowTest:
+ void SetUp() override {
+ BrowserWithTestWindowTest::SetUp();
+ static_cast<SigninUiUtilTestBrowserWindow*>(browser()->window())
+ ->set_browser(browser());
+ }
+
+ // BrowserWithTestWindowTest:
+ TestingProfile::TestingFactories GetTestingFactories() override {
+ return IdentityTestEnvironmentProfileAdaptor::
+ GetIdentityTestEnvironmentFactories();
+ }
+
+ // BrowserWithTestWindowTest:
+ std::unique_ptr<BrowserWindow> CreateBrowserWindow() override {
+ return std::make_unique<SigninUiUtilTestBrowserWindow>();
+ }
+
+ // Returns the identity manager.
+ signin::IdentityManager* GetIdentityManager() {
+ return IdentityManagerFactory::GetForProfile(profile());
+ }
+
+ void EnableSync(const AccountInfo& account_info,
+ bool is_default_promo_account) {
+ signin_ui_util::internal::EnableSyncFromPromo(
+ browser(), account_info, access_point_, is_default_promo_account,
+ base::BindOnce(&DiceSigninUiUtilTest::CreateDiceTurnSyncOnHelper,
+ base::Unretained(this)));
+ }
+
+ void ExpectNoSigninStartedHistograms(
+ const base::HistogramTester& histogram_tester) {
+ histogram_tester.ExpectTotalCount("Signin.SigninStartedAccessPoint", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.WithDefault", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NotDefault", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0);
+ }
+
+ void ExpectOneSigninStartedHistograms(
+ const base::HistogramTester& histogram_tester,
+ signin_metrics::PromoAction expected_promo_action) {
+ histogram_tester.ExpectUniqueSample("Signin.SigninStartedAccessPoint",
+ access_point_, 1);
+ switch (expected_promo_action) {
+ case signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO:
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NotDefault", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.WithDefault", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0);
+ break;
+ case signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT:
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NotDefault", 0);
+ histogram_tester.ExpectUniqueSample(
+ "Signin.SigninStartedAccessPoint.WithDefault", access_point_, 1);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0);
+ break;
+ case signin_metrics::PromoAction::PROMO_ACTION_NOT_DEFAULT:
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.WithDefault", 0);
+ histogram_tester.ExpectUniqueSample(
+ "Signin.SigninStartedAccessPoint.NotDefault", access_point_, 1);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0);
+ break;
+ case signin_metrics::PromoAction::
+ PROMO_ACTION_NEW_ACCOUNT_NO_EXISTING_ACCOUNT:
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.WithDefault", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NotDefault", 0);
+ histogram_tester.ExpectUniqueSample(
+ "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount",
+ access_point_, 1);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0);
+ break;
+ case signin_metrics::PromoAction::
+ PROMO_ACTION_NEW_ACCOUNT_EXISTING_ACCOUNT:
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.WithDefault", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NotDefault", 0);
+ histogram_tester.ExpectTotalCount(
+ "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0);
+ histogram_tester.ExpectUniqueSample(
+ "Signin.SigninStartedAccessPoint.NewAccountExistingAccount",
+ access_point_, 1);
+ break;
+ }
+ }
+
+ signin_metrics::AccessPoint access_point_ =
+ signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE;
+
+ bool create_dice_turn_sync_on_helper_called_ = false;
+ CreateDiceTurnSyncOnHelperParams create_dice_turn_sync_on_helper_params_;
+};
+
+TEST_F(DiceSigninUiUtilTest, EnableSyncWithExistingAccount) {
+ CoreAccountId account_id =
+ GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount(
+ kMainGaiaID, kMainEmail, "refresh_token", false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+
+ for (bool is_default_promo_account : {true, false}) {
+ base::HistogramTester histogram_tester;
+ base::UserActionTester user_action_tester;
+
+ ExpectNoSigninStartedHistograms(histogram_tester);
+ EXPECT_EQ(0, user_action_tester.GetActionCount(
+ "Signin_Signin_FromBookmarkBubble"));
+
+ EnableSync(
+ GetIdentityManager()->FindExtendedAccountInfoByAccountId(account_id),
+ is_default_promo_account);
+ signin_metrics::PromoAction expected_promo_action =
+ is_default_promo_account
+ ? signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT
+ : signin_metrics::PromoAction::PROMO_ACTION_NOT_DEFAULT;
+ ASSERT_TRUE(create_dice_turn_sync_on_helper_called_);
+ ExpectOneSigninStartedHistograms(histogram_tester, expected_promo_action);
+
+ EXPECT_EQ(1, user_action_tester.GetActionCount(
+ "Signin_Signin_FromBookmarkBubble"));
+ if (is_default_promo_account) {
+ EXPECT_EQ(1, user_action_tester.GetActionCount(
+ "Signin_SigninWithDefault_FromBookmarkBubble"));
+ } else {
+ EXPECT_EQ(1, user_action_tester.GetActionCount(
+ "Signin_SigninNotDefault_FromBookmarkBubble"));
+ }
+
+ // Verify that the helper to enable sync is created with the expected
+ // params.
+ EXPECT_EQ(profile(), create_dice_turn_sync_on_helper_params_.profile);
+ EXPECT_EQ(browser(), create_dice_turn_sync_on_helper_params_.browser);
+ EXPECT_EQ(account_id, create_dice_turn_sync_on_helper_params_.account_id);
+ EXPECT_EQ(signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE,
+ create_dice_turn_sync_on_helper_params_.signin_access_point);
+ EXPECT_EQ(expected_promo_action,
+ create_dice_turn_sync_on_helper_params_.signin_promo_action);
+ EXPECT_EQ(signin_metrics::Reason::kSigninPrimaryAccount,
+ create_dice_turn_sync_on_helper_params_.signin_reason);
+ EXPECT_EQ(DiceTurnSyncOnHelper::SigninAbortedMode::KEEP_ACCOUNT,
+ create_dice_turn_sync_on_helper_params_.signin_aborted_mode);
+ }
+}
+
+TEST_F(DiceSigninUiUtilTest, EnableSyncWithAccountThatNeedsReauth) {
+ AddTab(browser(), GURL("http://example.com"));
+ CoreAccountId account_id =
+ GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount(
+ kMainGaiaID, kMainEmail, "refresh_token", false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+
+ // Add an account and then put its refresh token into an error state to
+ // require a reauth before enabling sync.
+ signin::UpdatePersistentErrorOfRefreshTokenForAccount(
+ GetIdentityManager(), account_id,
+ GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS));
+
+ for (bool is_default_promo_account : {true, false}) {
+ base::HistogramTester histogram_tester;
+ base::UserActionTester user_action_tester;
+
+ ExpectNoSigninStartedHistograms(histogram_tester);
+ EXPECT_EQ(0, user_action_tester.GetActionCount(
+ "Signin_Signin_FromBookmarkBubble"));
+
+ EnableSync(
+ GetIdentityManager()->FindExtendedAccountInfoByAccountId(account_id),
+ is_default_promo_account);
+ ASSERT_FALSE(create_dice_turn_sync_on_helper_called_);
+
+ ExpectOneSigninStartedHistograms(
+ histogram_tester,
+ is_default_promo_account
+ ? signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT
+ : signin_metrics::PromoAction::PROMO_ACTION_NOT_DEFAULT);
+ EXPECT_EQ(1, user_action_tester.GetActionCount(
+ "Signin_Signin_FromBookmarkBubble"));
+
+ if (is_default_promo_account) {
+ EXPECT_EQ(1, user_action_tester.GetActionCount(
+ "Signin_SigninWithDefault_FromBookmarkBubble"));
+ } else {
+ EXPECT_EQ(1, user_action_tester.GetActionCount(
+ "Signin_SigninNotDefault_FromBookmarkBubble"));
+ }
+
+ // Verify that the active tab has the correct DICE sign-in URL.
+ TabStripModel* tab_strip = browser()->tab_strip_model();
+ content::WebContents* active_contents = tab_strip->GetActiveWebContents();
+ ASSERT_TRUE(active_contents);
+ EXPECT_EQ(signin::GetChromeSyncURLForDice(kMainEmail,
+ google_util::kGoogleHomepageURL),
+ active_contents->GetVisibleURL());
+ tab_strip->CloseWebContentsAt(
+ tab_strip->GetIndexOfWebContents(active_contents),
+ TabStripModel::CLOSE_USER_GESTURE);
+ }
+}
+
+TEST_F(DiceSigninUiUtilTest, EnableSyncForNewAccountWithNoTab) {
+ base::HistogramTester histogram_tester;
+ base::UserActionTester user_action_tester;
+
+ ExpectNoSigninStartedHistograms(histogram_tester);
+ EXPECT_EQ(
+ 0, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+
+ EnableSync(AccountInfo(), false /* is_default_promo_account (not used)*/);
+ ASSERT_FALSE(create_dice_turn_sync_on_helper_called_);
+
+ ExpectOneSigninStartedHistograms(
+ histogram_tester, signin_metrics::PromoAction::
+ PROMO_ACTION_NEW_ACCOUNT_NO_EXISTING_ACCOUNT);
+ EXPECT_EQ(
+ 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+ EXPECT_EQ(1,
+ user_action_tester.GetActionCount(
+ "Signin_SigninNewAccountNoExistingAccount_FromBookmarkBubble"));
+
+ // Verify that the active tab has the correct DICE sign-in URL.
+ content::WebContents* active_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+ ASSERT_TRUE(active_contents);
+ EXPECT_EQ(
+ signin::GetChromeSyncURLForDice("", google_util::kGoogleHomepageURL),
+ active_contents->GetVisibleURL());
+}
+
+TEST_F(DiceSigninUiUtilTest, EnableSyncForNewAccountWithNoTabWithExisting) {
+ base::HistogramTester histogram_tester;
+ base::UserActionTester user_action_tester;
+
+ GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount(
+ kMainGaiaID, kMainEmail, "refresh_token", false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+
+ ExpectNoSigninStartedHistograms(histogram_tester);
+ EXPECT_EQ(
+ 0, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+
+ EnableSync(AccountInfo(), false /* is_default_promo_account (not used)*/);
+ ASSERT_FALSE(create_dice_turn_sync_on_helper_called_);
+
+ ExpectOneSigninStartedHistograms(
+ histogram_tester,
+ signin_metrics::PromoAction::PROMO_ACTION_NEW_ACCOUNT_EXISTING_ACCOUNT);
+ EXPECT_EQ(
+ 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+ EXPECT_EQ(1,
+ user_action_tester.GetActionCount(
+ "Signin_SigninNewAccountExistingAccount_FromBookmarkBubble"));
+}
+
+TEST_F(DiceSigninUiUtilTest, EnableSyncForNewAccountWithOneTab) {
+ base::HistogramTester histogram_tester;
+ base::UserActionTester user_action_tester;
+ AddTab(browser(), GURL("http://foo/1"));
+
+ ExpectNoSigninStartedHistograms(histogram_tester);
+ EXPECT_EQ(
+ 0, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+
+ EnableSync(AccountInfo(), false /* is_default_promo_account (not used)*/);
+ ASSERT_FALSE(create_dice_turn_sync_on_helper_called_);
+
+ ExpectOneSigninStartedHistograms(
+ histogram_tester, signin_metrics::PromoAction::
+ PROMO_ACTION_NEW_ACCOUNT_NO_EXISTING_ACCOUNT);
+ EXPECT_EQ(
+ 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+ EXPECT_EQ(1,
+ user_action_tester.GetActionCount(
+ "Signin_SigninNewAccountNoExistingAccount_FromBookmarkBubble"));
+
+ // Verify that the active tab has the correct DICE sign-in URL.
+ content::WebContents* active_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+ ASSERT_TRUE(active_contents);
+ EXPECT_EQ(
+ signin::GetChromeSyncURLForDice("", google_util::kGoogleHomepageURL),
+ active_contents->GetVisibleURL());
+}
+
+TEST_F(DiceSigninUiUtilTest, GetAccountsForDicePromos) {
+ // Should start off with no accounts.
+ std::vector<AccountInfo> accounts = GetAccountsForDicePromos(profile());
+ EXPECT_TRUE(accounts.empty());
+
+ // TODO(tangltom): Flesh out this test.
+}
+
+TEST_F(DiceSigninUiUtilTest, MergeDiceSigninTab) {
+ base::UserActionTester user_action_tester;
+ EnableSync(AccountInfo(), false);
+ EXPECT_EQ(
+ 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+
+ // Signin tab is reused.
+ EnableSync(AccountInfo(), false);
+ EXPECT_EQ(
+ 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+
+ // Give focus to a different tab.
+ TabStripModel* tab_strip = browser()->tab_strip_model();
+ ASSERT_EQ(0, tab_strip->active_index());
+ GURL other_url = GURL("http://example.com");
+ AddTab(browser(), other_url);
+ tab_strip->ActivateTabAt(0, {TabStripModel::GestureType::kOther});
+ ASSERT_EQ(other_url, tab_strip->GetActiveWebContents()->GetVisibleURL());
+ ASSERT_EQ(0, tab_strip->active_index());
+
+ // Extensions re-use the tab but do not take focus.
+ access_point_ = signin_metrics::AccessPoint::ACCESS_POINT_EXTENSIONS;
+ EnableSync(AccountInfo(), false);
+ EXPECT_EQ(
+ 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+ EXPECT_EQ(0, tab_strip->active_index());
+
+ // Other access points re-use the tab and take focus.
+ access_point_ = signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS;
+ EnableSync(AccountInfo(), false);
+ EXPECT_EQ(
+ 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble"));
+ EXPECT_EQ(1, tab_strip->active_index());
+}
+
+TEST_F(DiceSigninUiUtilTest, ShowReauthTab) {
+ AddTab(browser(), GURL("http://example.com"));
+ AccountInfo account_info = signin::MakePrimaryAccountAvailable(
+ GetIdentityManager(), "foo@example.com", signin::ConsentLevel::kSync);
+
+ // Add an account and then put its refresh token into an error state to
+ // require a reauth before enabling sync.
+ signin::UpdatePersistentErrorOfRefreshTokenForAccount(
+ GetIdentityManager(), account_info.account_id,
+ GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS));
+
+ signin_ui_util::ShowReauthForPrimaryAccountWithAuthError(
+ browser(),
+ signin_metrics::AccessPoint::ACCESS_POINT_AVATAR_BUBBLE_SIGN_IN);
+
+ // Verify that the active tab has the correct DICE sign-in URL.
+ TabStripModel* tab_strip = browser()->tab_strip_model();
+ content::WebContents* active_contents = tab_strip->GetActiveWebContents();
+ ASSERT_TRUE(active_contents);
+ EXPECT_EQ(signin::GetChromeSyncURLForDice(account_info.email,
+ google_util::kGoogleHomepageURL),
+ active_contents->GetVisibleURL());
+}
+
+TEST_F(DiceSigninUiUtilTest,
+ ShouldShowAnimatedIdentityOnOpeningWindow_ReturnsTrueForMultiProfiles) {
+ const char kSecondProfile[] = "SecondProfile";
+ const char16_t kSecondProfile16[] = u"SecondProfile";
+ const base::FilePath profile_path =
+ profile_manager()->profiles_dir().AppendASCII(kSecondProfile);
+ ProfileAttributesInitParams params;
+ params.profile_path = profile_path;
+ params.profile_name = kSecondProfile16;
+ profile_manager()->profile_attributes_storage()->AddProfile(
+ std::move(params));
+
+ EXPECT_TRUE(ShouldShowAnimatedIdentityOnOpeningWindow(
+ *profile_manager()->profile_attributes_storage(), profile()));
+}
+
+TEST_F(DiceSigninUiUtilTest,
+ ShouldShowAnimatedIdentityOnOpeningWindow_ReturnsTrueForMultiSignin) {
+ GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount(
+ kMainGaiaID, kMainEmail, "refresh_token", false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+ GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount(
+ kSecondaryGaiaID, kSecondaryEmail, "refresh_token", false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+
+ EXPECT_TRUE(ShouldShowAnimatedIdentityOnOpeningWindow(
+ *profile_manager()->profile_attributes_storage(), profile()));
+
+ // The identity can be shown again immediately (which is what happens if there
+ // is multiple windows at startup).
+ RecordAnimatedIdentityTriggered(profile());
+ EXPECT_TRUE(ShouldShowAnimatedIdentityOnOpeningWindow(
+ *profile_manager()->profile_attributes_storage(), profile()));
+}
+
+TEST_F(
+ DiceSigninUiUtilTest,
+ ShouldShowAnimatedIdentityOnOpeningWindow_ReturnsFalseForSingleProfileSingleSignin) {
+ GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount(
+ kMainGaiaID, kMainEmail, "refresh_token", false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+
+ EXPECT_FALSE(ShouldShowAnimatedIdentityOnOpeningWindow(
+ *profile_manager()->profile_attributes_storage(), profile()));
+}
+
+TEST_F(DiceSigninUiUtilTest, ShowExtensionSigninPrompt) {
+ Profile* profile = browser()->profile();
+ TabStripModel* tab_strip = browser()->tab_strip_model();
+ ShowExtensionSigninPrompt(profile, /*enable_sync=*/true,
+ /*email_hint=*/std::string());
+ EXPECT_EQ(1, tab_strip->count());
+ // Calling the function again reuses the tab.
+ ShowExtensionSigninPrompt(profile, /*enable_sync=*/true,
+ /*email_hint=*/std::string());
+ EXPECT_EQ(1, tab_strip->count());
+
+ content::WebContents* tab = tab_strip->GetWebContentsAt(0);
+ ASSERT_TRUE(tab);
+ EXPECT_TRUE(base::StartsWith(
+ tab->GetVisibleURL().spec(),
+ GaiaUrls::GetInstance()->signin_chrome_sync_dice().spec(),
+ base::CompareCase::INSENSITIVE_ASCII));
+
+ // Changing the parameter opens a new tab.
+ ShowExtensionSigninPrompt(profile, /*enable_sync=*/false,
+ /*email_hint=*/std::string());
+ EXPECT_EQ(2, tab_strip->count());
+ // Calling the function again reuses the tab.
+ ShowExtensionSigninPrompt(profile, /*enable_sync=*/false,
+ /*email_hint=*/std::string());
+ EXPECT_EQ(2, tab_strip->count());
+ tab = tab_strip->GetWebContentsAt(1);
+ ASSERT_TRUE(tab);
+ EXPECT_TRUE(
+ base::StartsWith(tab->GetVisibleURL().spec(),
+ GaiaUrls::GetInstance()->add_account_url().spec(),
+ base::CompareCase::INSENSITIVE_ASCII));
+}
+
+TEST_F(DiceSigninUiUtilTest, ShowExtensionSigninPrompt_AsLockedProfile) {
+ signin_util::ScopedForceSigninSetterForTesting force_signin_setter(true);
+ Profile* profile = browser()->profile();
+ ProfileAttributesEntry* entry =
+ g_browser_process->profile_manager()
+ ->GetProfileAttributesStorage()
+ .GetProfileAttributesWithPath(profile->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->LockForceSigninProfile(true);
+ TabStripModel* tab_strip = browser()->tab_strip_model();
+ ShowExtensionSigninPrompt(profile, /*enable_sync=*/true,
+ /*email_hint=*/std::string());
+ EXPECT_EQ(0, tab_strip->count());
+ ShowExtensionSigninPrompt(profile, /*enable_sync=*/false,
+ /*email_hint=*/std::string());
+ EXPECT_EQ(0, tab_strip->count());
+}
+#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
+
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+class MirrorSigninUiUtilTest : public BrowserWithTestWindowTest {
+ public:
+ MirrorSigninUiUtilTest() = default;
+ ~MirrorSigninUiUtilTest() override = default;
+
+ // BrowserWithTestWindowTest:
+ TestingProfile::TestingFactories GetTestingFactories() override {
+ return IdentityTestEnvironmentProfileAdaptor::
+ GetIdentityTestEnvironmentFactories();
+ }
+};
+
+TEST_F(MirrorSigninUiUtilTest, ShowReauthDialog) {
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile());
+ const std::string kEmail = "foo@example.com";
+ AccountInfo account_info = signin::MakePrimaryAccountAvailable(
+ identity_manager, kEmail, signin::ConsentLevel::kSync);
+
+ // Add an account and then put its refresh token into an error state to
+ // require a reauth before enabling sync.
+ signin::UpdatePersistentErrorOfRefreshTokenForAccount(
+ identity_manager, account_info.account_id,
+ GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS));
+
+ account_manager::MockAccountManagerFacade mock_facade;
+
+ EXPECT_CALL(mock_facade,
+ ShowReauthAccountDialog(
+ account_manager::AccountManagerFacade::AccountAdditionSource::
+ kAvatarBubbleReauthAccountButton,
+ kEmail));
+ internal::ShowReauthForPrimaryAccountWithAuthErrorLacros(
+ browser(),
+ signin_metrics::AccessPoint::ACCESS_POINT_AVATAR_BUBBLE_SIGN_IN,
+ &mock_facade);
+}
+
+TEST_F(MirrorSigninUiUtilTest, ShowExtensionSigninPrompt) {
+ const std::string kEmail = "foo@example.com";
+ TabStripModel* tab_strip = browser()->tab_strip_model();
+ account_manager::MockAccountManagerFacade mock_facade;
+
+ EXPECT_CALL(
+ mock_facade,
+ ShowReauthAccountDialog(account_manager::AccountManagerFacade::
+ AccountAdditionSource::kChromeExtensionReauth,
+ kEmail));
+ internal::ShowExtensionSigninPrompt(browser()->profile(), &mock_facade,
+ /*enable_sync=*/true, kEmail);
+ // No tabs should be opened.
+ EXPECT_EQ(0, tab_strip->count());
+}
+
+TEST_F(MirrorSigninUiUtilTest, ShowExtensionSigninPrompt_AsLockedProfile) {
+ signin_util::ScopedForceSigninSetterForTesting force_signin_setter(true);
+ Profile* profile = browser()->profile();
+ ProfileAttributesEntry* entry =
+ g_browser_process->profile_manager()
+ ->GetProfileAttributesStorage()
+ .GetProfileAttributesWithPath(profile->GetPath());
+ ASSERT_NE(entry, nullptr);
+ entry->LockForceSigninProfile(true);
+
+ const std::string kEmail = "foo@example.com";
+ TabStripModel* tab_strip = browser()->tab_strip_model();
+ account_manager::MockAccountManagerFacade mock_facade;
+
+ EXPECT_CALL(mock_facade, ShowReauthAccountDialog(testing::_, testing::_))
+ .Times(0);
+ internal::ShowExtensionSigninPrompt(browser()->profile(), &mock_facade,
+ /*enable_sync=*/true, kEmail);
+ // No dialogs and tabs should be opened.
+ EXPECT_EQ(0, tab_strip->count());
+}
+
+#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
+
+// This test does not use the DiceSigninUiUtilTest test fixture, because it
+// needs a mock time environment, and BrowserWithTestWindowTest may be flaky
+// when used with mock time (see https://crbug.com/1014790).
+TEST(ShouldShowAnimatedIdentityOnOpeningWindow, ReturnsFalseForNewWindow) {
+ // Setup a testing profile manager with mock time.
+ content::BrowserTaskEnvironment task_environment(
+ base::test::TaskEnvironment::TimeSource::MOCK_TIME);
+ ScopedTestingLocalState local_state(TestingBrowserProcess::GetGlobal());
+ TestingProfileManager profile_manager(TestingBrowserProcess::GetGlobal(),
+ &local_state);
+ ASSERT_TRUE(profile_manager.SetUp());
+ std::string name("testing_profile");
+ TestingProfile* profile = profile_manager.CreateTestingProfile(
+ name, std::unique_ptr<sync_preferences::PrefServiceSyncable>(),
+ base::UTF8ToUTF16(name), 0, std::string(),
+ IdentityTestEnvironmentProfileAdaptor::
+ GetIdentityTestEnvironmentFactories());
+
+ // Setup accounts.
+ signin::IdentityManager* identity_manager =
+ IdentityManagerFactory::GetForProfile(profile);
+ identity_manager->GetAccountsMutator()->AddOrUpdateAccount(
+ kMainGaiaID, kMainEmail, "refresh_token", false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+ identity_manager->GetAccountsMutator()->AddOrUpdateAccount(
+ kSecondaryGaiaID, kSecondaryEmail, "refresh_token", false,
+ signin_metrics::SourceForRefreshTokenOperation::kUnknown);
+ EXPECT_TRUE(ShouldShowAnimatedIdentityOnOpeningWindow(
+ *profile_manager.profile_attributes_storage(), profile));
+
+ // Animation is shown once.
+ RecordAnimatedIdentityTriggered(profile);
+
+ // Wait a few seconds.
+ task_environment.FastForwardBy(base::Seconds(6));
+
+ // Animation is not shown again in a new window.
+ EXPECT_FALSE(ShouldShowAnimatedIdentityOnOpeningWindow(
+ *profile_manager.profile_attributes_storage(), profile));
+}
+
+} // namespace signin_ui_util
diff --git a/chromium/chrome/browser/signin/signin_util.cc b/chromium/chrome/browser/signin/signin_util.cc
new file mode 100644
index 00000000000..d3ab11f8671
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_util.cc
@@ -0,0 +1,382 @@
+// Copyright 2017 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.
+
+#include "chrome/browser/signin/signin_util.h"
+
+#include <memory>
+
+#include "base/bind.h"
+#include "base/feature_list.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/strings/string_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/supports_user_data.h"
+#include "base/task/post_task.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/policy/cloud/user_policy_signin_service_internal.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profiles_state.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "chrome/browser/ui/simple_message_box.h"
+#include "chrome/browser/ui/startup/startup_types.h"
+#include "chrome/browser/ui/webui/profile_helper.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/common/url_constants.h"
+#include "chrome/grit/generated_resources.h"
+#include "components/google/core/common/google_util.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_utils.h"
+#include "components/signin/public/identity_manager/primary_account_mutator.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+#include "ui/base/l10n/l10n_util.h"
+
+#if defined(OS_WIN) || defined(OS_LINUX) || defined(OS_CHROMEOS) || \
+ defined(OS_MAC)
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/browser/ui/browser_list_observer.h"
+#include "chrome/browser/ui/browser_window.h"
+#define CAN_DELETE_PROFILE
+#endif
+
+namespace signin_util {
+namespace {
+
+constexpr char kSignoutSettingKey[] = "signout_setting";
+
+#if defined(CAN_DELETE_PROFILE)
+// Manager that presents the profile will be deleted dialog on the first active
+// browser window.
+class DeleteProfileDialogManager : public BrowserListObserver {
+ public:
+ class Delegate {
+ public:
+ // Called when the profile was marked for deletion. It is safe for the
+ // delegate to delete |manager| when this is called.
+ virtual void OnProfileDeleted(DeleteProfileDialogManager* manager) = 0;
+ };
+
+ DeleteProfileDialogManager(std::string primary_account_email,
+ Delegate* delegate)
+ : primary_account_email_(primary_account_email), delegate_(delegate) {}
+
+ DeleteProfileDialogManager(const DeleteProfileDialogManager&) = delete;
+ DeleteProfileDialogManager& operator=(const DeleteProfileDialogManager&) =
+ delete;
+
+ ~DeleteProfileDialogManager() override { BrowserList::RemoveObserver(this); }
+
+ void PresentDialogOnAllBrowserWindows(Profile* profile) {
+ DCHECK(profile);
+ DCHECK(profile_path_.empty());
+ profile_path_ = profile->GetPath();
+
+ BrowserList::AddObserver(this);
+ Browser* active_browser = chrome::FindLastActiveWithProfile(profile);
+ if (active_browser)
+ OnBrowserSetLastActive(active_browser);
+ }
+
+ void OnBrowserSetLastActive(Browser* browser) override {
+ DCHECK(!profile_path_.empty());
+
+ if (profile_path_ != browser->profile()->GetPath())
+ return;
+
+ active_browser_ = browser;
+
+ // Display the dialog on the next run loop as otherwise the dialog can block
+ // browser from displaying because the dialog creates a nested run loop.
+ //
+ // This happens because the browser window is not fully created yet when
+ // OnBrowserSetLastActive() is called. To finish the creation, the code
+ // needs to return from OnBrowserSetLastActive().
+ //
+ // However, if we open a warning dialog from OnBrowserSetLastActive()
+ // synchronously, it will create a nested run loop that will not return
+ // from OnBrowserSetLastActive() until the dialog is dismissed. But the user
+ // cannot dismiss the dialog because the browser is not even shown!
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&DeleteProfileDialogManager::ShowDeleteProfileDialog,
+ weak_factory_.GetWeakPtr(), browser));
+ }
+
+ // Called immediately after a browser becomes not active.
+ void OnBrowserNoLongerActive(Browser* browser) override {
+ if (active_browser_ == browser)
+ active_browser_ = nullptr;
+ }
+
+ void OnBrowserRemoved(Browser* browser) override {
+ if (active_browser_ == browser)
+ active_browser_ = nullptr;
+ }
+
+ private:
+ void ShowDeleteProfileDialog(Browser* browser) {
+ // Block opening dialog from nested task.
+ static bool is_dialog_shown = false;
+ if (is_dialog_shown)
+ return;
+ base::AutoReset<bool> auto_reset(&is_dialog_shown, true);
+
+ // Check that |browser| is still active.
+ if (!active_browser_ || active_browser_ != browser)
+ return;
+
+ // Show the dialog.
+ DCHECK(browser->window()->GetNativeWindow());
+ chrome::MessageBoxResult result = chrome::ShowWarningMessageBox(
+ browser->window()->GetNativeWindow(),
+ l10n_util::GetStringUTF16(IDS_PROFILE_WILL_BE_DELETED_DIALOG_TITLE),
+ l10n_util::GetStringFUTF16(
+ IDS_PROFILE_WILL_BE_DELETED_DIALOG_DESCRIPTION,
+ base::ASCIIToUTF16(primary_account_email_),
+ base::ASCIIToUTF16(
+ gaia::ExtractDomainName(primary_account_email_))));
+
+ switch (result) {
+ case chrome::MessageBoxResult::MESSAGE_BOX_RESULT_NO: {
+ // If the warning dialog is automatically dismissed or the user closed
+ // the dialog by clicking on the close "X" button, then re-present the
+ // dialog (the user should not be able to interact with the browser
+ // window as the profile must be deleted).
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&DeleteProfileDialogManager::ShowDeleteProfileDialog,
+ weak_factory_.GetWeakPtr(), browser));
+ break;
+ }
+ case chrome::MessageBoxResult::MESSAGE_BOX_RESULT_YES:
+ webui::DeleteProfileAtPath(
+ profile_path_,
+ ProfileMetrics::DELETE_PROFILE_PRIMARY_ACCOUNT_NOT_ALLOWED);
+ delegate_->OnProfileDeleted(this);
+ // |this| may be destroyed at this point. Avoid using it.
+ break;
+ case chrome::MessageBoxResult::MESSAGE_BOX_RESULT_DEFERRED:
+ NOTREACHED() << "Message box must not return deferred result when run "
+ "synchronously";
+ break;
+ }
+ }
+
+ std::string primary_account_email_;
+ raw_ptr<Delegate> delegate_;
+ base::FilePath profile_path_;
+ raw_ptr<Browser> active_browser_;
+ base::WeakPtrFactory<DeleteProfileDialogManager> weak_factory_{this};
+};
+#endif // defined(CAN_DELETE_PROFILE)
+
+// Per-profile manager for the signout allowed setting.
+#if defined(CAN_DELETE_PROFILE)
+class UserSignoutSetting : public base::SupportsUserData::Data,
+ public DeleteProfileDialogManager::Delegate {
+#else
+class UserSignoutSetting : public base::SupportsUserData::Data {
+#endif // defined(CAN_DELETE_PROFILE)
+ public:
+ enum class State { kUndefined, kAllowed, kDisallowed };
+
+ // Fetch from Profile. Make and store if not already present.
+ static UserSignoutSetting* GetForProfile(Profile* profile) {
+ UserSignoutSetting* signout_setting = static_cast<UserSignoutSetting*>(
+ profile->GetUserData(kSignoutSettingKey));
+
+ if (!signout_setting) {
+ profile->SetUserData(kSignoutSettingKey,
+ std::make_unique<UserSignoutSetting>());
+ signout_setting = static_cast<UserSignoutSetting*>(
+ profile->GetUserData(kSignoutSettingKey));
+ }
+
+ return signout_setting;
+ }
+
+ State state() const { return state_; }
+ void set_state(State state) { state_ = state; }
+
+#if defined(CAN_DELETE_PROFILE)
+ // Shows the delete profile dialog on the first browser active window.
+ void ShowDeleteProfileDialog(Profile* profile, const std::string& email) {
+ if (delete_profile_dialog_manager_)
+ return;
+ delete_profile_dialog_manager_ =
+ std::make_unique<DeleteProfileDialogManager>(email, this);
+ delete_profile_dialog_manager_->PresentDialogOnAllBrowserWindows(profile);
+ }
+
+ void OnProfileDeleted(DeleteProfileDialogManager* dialog_manager) override {
+ DCHECK_EQ(delete_profile_dialog_manager_.get(), dialog_manager);
+ delete_profile_dialog_manager_.reset();
+ }
+#endif
+
+ private:
+ State state_ = State::kUndefined;
+
+#if defined(CAN_DELETE_PROFILE)
+ std::unique_ptr<DeleteProfileDialogManager> delete_profile_dialog_manager_;
+#endif
+};
+
+enum ForceSigninPolicyCache {
+ NOT_CACHED = 0,
+ ENABLE,
+ DISABLE
+} g_is_force_signin_enabled_cache = NOT_CACHED;
+
+void SetForceSigninPolicy(bool enable) {
+ g_is_force_signin_enabled_cache = enable ? ENABLE : DISABLE;
+}
+
+} // namespace
+
+ScopedForceSigninSetterForTesting::ScopedForceSigninSetterForTesting(
+ bool enable) {
+ SetForceSigninForTesting(enable); // IN-TEST
+}
+
+ScopedForceSigninSetterForTesting::~ScopedForceSigninSetterForTesting() {
+ ResetForceSigninForTesting(); // IN-TEST
+}
+
+bool IsForceSigninEnabled() {
+ if (g_is_force_signin_enabled_cache == NOT_CACHED) {
+ PrefService* prefs = g_browser_process->local_state();
+ if (prefs)
+ SetForceSigninPolicy(prefs->GetBoolean(prefs::kForceBrowserSignin));
+ else
+ return false;
+ }
+ return (g_is_force_signin_enabled_cache == ENABLE);
+}
+
+void SetForceSigninForTesting(bool enable) {
+ SetForceSigninPolicy(enable);
+}
+
+void ResetForceSigninForTesting() {
+ g_is_force_signin_enabled_cache = NOT_CACHED;
+}
+
+bool IsUserSignoutAllowedForProfile(Profile* profile) {
+ return UserSignoutSetting::GetForProfile(profile)->state() ==
+ UserSignoutSetting::State::kAllowed;
+}
+
+void EnsureUserSignoutAllowedIsInitializedForProfile(Profile* profile) {
+ if (UserSignoutSetting::GetForProfile(profile)->state() ==
+ UserSignoutSetting::State::kUndefined) {
+ SetUserSignoutAllowedForProfile(profile, true);
+ }
+}
+
+void SetUserSignoutAllowedForProfile(Profile* profile, bool is_allowed) {
+ UserSignoutSetting::State new_state =
+ is_allowed ? UserSignoutSetting::State::kAllowed
+ : UserSignoutSetting::State::kDisallowed;
+ UserSignoutSetting::GetForProfile(profile)->set_state(new_state);
+}
+
+void EnsurePrimaryAccountAllowedForProfile(Profile* profile) {
+// All primary accounts are allowed on ChromeOS, so this method is a no-op on
+// ChromeOS.
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+ auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
+ if (!identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync))
+ return;
+
+ CoreAccountInfo primary_account =
+ identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSync);
+ if (profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed) &&
+ signin::IsUsernameAllowedByPatternFromPrefs(
+ g_browser_process->local_state(), primary_account.email)) {
+ return;
+ }
+
+ UserSignoutSetting* signout_setting =
+ UserSignoutSetting::GetForProfile(profile);
+ switch (signout_setting->state()) {
+ case UserSignoutSetting::State::kUndefined:
+ NOTREACHED();
+ break;
+ case UserSignoutSetting::State::kAllowed: {
+ // Force clear the primary account if it is no longer allowed and if sign
+ // out is allowed.
+ auto* primary_account_mutator =
+ identity_manager->GetPrimaryAccountMutator();
+ primary_account_mutator->ClearPrimaryAccount(
+ signin_metrics::SIGNIN_NOT_ALLOWED_ON_PROFILE_INIT,
+ signin_metrics::SignoutDelete::kIgnoreMetric);
+ break;
+ }
+ case UserSignoutSetting::State::kDisallowed:
+#if defined(CAN_DELETE_PROFILE)
+ // Force remove the profile if sign out is not allowed and if the
+ // primary account is no longer allowed.
+ // This may be called while the profile is initializing, so it must be
+ // scheduled for later to allow the profile initialization to complete.
+ CHECK(profiles::IsMultipleProfilesEnabled());
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&UserSignoutSetting::ShowDeleteProfileDialog,
+ base::Unretained(signout_setting), profile,
+ primary_account.email));
+#else
+ CHECK(false) << "Deleting profiles is not supported.";
+#endif // defined(CAN_DELETE_PROFILE)
+ break;
+ }
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
+}
+
+#if !defined(OS_ANDROID)
+bool ProfileSeparationEnforcedByPolicy(
+ Profile* profile,
+ const std::string& intercepted_account_level_policy_value) {
+ if (!base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync))
+ return false;
+ std::string current_profile_account_restriction =
+ profile->GetPrefs()->GetString(prefs::kManagedAccountsSigninRestriction);
+
+ bool is_machine_level_policy = profile->GetPrefs()->GetBoolean(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine);
+
+ // Enforce profile separation for all new signins if any restriction is
+ // applied at a machine level.
+ if (is_machine_level_policy) {
+ return !current_profile_account_restriction.empty() &&
+ current_profile_account_restriction != "none";
+ }
+
+ // Enforce profile separation for all new signins if "primary_account_strict"
+ // is set at the user account level.
+ return current_profile_account_restriction == "primary_account_strict" ||
+ base::StartsWith(intercepted_account_level_policy_value,
+ "primary_account");
+}
+
+void RecordEnterpriseProfileCreationUserChoice(bool enforced_by_policy,
+ bool created) {
+ base::UmaHistogramBoolean(
+ enforced_by_policy
+ ? "Signin.Enterprise.WorkProfile.ProfileCreatedWithPolicySet"
+ : "Signin.Enterprise.WorkProfile.ProfileCreatedwithPolicyUnset",
+ created);
+}
+
+#endif
+
+} // namespace signin_util
diff --git a/chromium/chrome/browser/signin/signin_util.h b/chromium/chrome/browser/signin/signin_util.h
new file mode 100644
index 00000000000..10a436d9ea1
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_util.h
@@ -0,0 +1,73 @@
+// Copyright 2017 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_SIGNIN_SIGNIN_UTIL_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_H_
+
+#include <string>
+
+#include "build/build_config.h"
+
+class Profile;
+
+namespace signin_util {
+
+// This class calls ResetForceSigninForTesting when destroyed, so that
+// ForcedSigning doesn't leak across tests.
+class ScopedForceSigninSetterForTesting {
+ public:
+ explicit ScopedForceSigninSetterForTesting(bool enable);
+ ~ScopedForceSigninSetterForTesting();
+};
+
+// Return whether the force sign in policy is enabled or not.
+// The state of this policy will not be changed without relaunch Chrome.
+bool IsForceSigninEnabled();
+
+// Enable or disable force sign in for testing. Please use
+// ScopedForceSigninSetterForTesting instead, if possible. If not, make sure
+// ResetForceSigninForTesting is called before the test finishes.
+void SetForceSigninForTesting(bool enable);
+
+// Reset force sign in to uninitialized state for testing.
+void ResetForceSigninForTesting();
+
+// Returns true if clearing the primary profile is allowed.
+bool IsUserSignoutAllowedForProfile(Profile* profile);
+
+// Sign-out is allowed by default, but some Chrome profiles (e.g. for cloud-
+// managed enterprise accounts) may wish to disallow user-initiated sign-out.
+// Note that this exempts sign-outs that are not user-initiated (e.g. sign-out
+// triggered when cloud policy no longer allows current email pattern). See
+// ChromeSigninClient::PreSignOut().
+void SetUserSignoutAllowedForProfile(Profile* profile, bool is_allowed);
+
+// Updates the user sign-out state to |true| if is was never initialized.
+// This should be called at the end of the flow to initialize a profile to
+// ensure that the signout allowed flag is updated.
+void EnsureUserSignoutAllowedIsInitializedForProfile(Profile* profile);
+
+// Ensures that the primary account for |profile| is allowed:
+// * If profile does not have any primary account, then this is a no-op.
+// * If |IsUserSignoutAllowedForProfile| is allowed and the primary account
+// is no longer allowed, then this clears the primary account.
+// * If |IsUserSignoutAllowedForProfile| is not allowed and the primary account
+// is not longer allowed, then this removes the profile.
+void EnsurePrimaryAccountAllowedForProfile(Profile* profile);
+
+#if !defined(OS_ANDROID)
+// Returns true if profile separation is enforced by policy.
+bool ProfileSeparationEnforcedByPolicy(
+ Profile* profile,
+ const std::string& intercepted_account_level_policy_value);
+
+// Records a UMA metric if the user accepts or not to create an enterprise
+// profile.
+void RecordEnterpriseProfileCreationUserChoice(bool enforced_by_policy,
+ bool created);
+#endif
+
+} // namespace signin_util
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_H_
diff --git a/chromium/chrome/browser/signin/signin_util_unittest.cc b/chromium/chrome/browser/signin/signin_util_unittest.cc
new file mode 100644
index 00000000000..80862578339
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_util_unittest.cc
@@ -0,0 +1,127 @@
+// Copyright 2017 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.
+
+#include "chrome/browser/signin/signin_util.h"
+
+#include <memory>
+
+#include "base/feature_list.h"
+#include "build/buildflag.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/prefs/browser_prefs.h"
+#include "chrome/browser/signin/signin_features.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/browser_with_test_window_test.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/prefs/pref_service.h"
+#include "components/prefs/testing_pref_service.h"
+
+class SigninUtilTest : public BrowserWithTestWindowTest {
+ public:
+ void SetUp() override {
+ BrowserWithTestWindowTest::SetUp();
+ signin_util::ResetForceSigninForTesting();
+ }
+
+ void TearDown() override {
+ signin_util::ResetForceSigninForTesting();
+ BrowserWithTestWindowTest::TearDown();
+ }
+};
+
+TEST_F(SigninUtilTest, GetForceSigninPolicy) {
+ EXPECT_FALSE(signin_util::IsForceSigninEnabled());
+
+ g_browser_process->local_state()->SetBoolean(prefs::kForceBrowserSignin,
+ true);
+ signin_util::ResetForceSigninForTesting();
+ EXPECT_TRUE(signin_util::IsForceSigninEnabled());
+ g_browser_process->local_state()->SetBoolean(prefs::kForceBrowserSignin,
+ false);
+ signin_util::ResetForceSigninForTesting();
+ EXPECT_FALSE(signin_util::IsForceSigninEnabled());
+}
+
+#if !BUILDFLAG(IS_CHROMEOS_LACROS)
+class SigninUtilEnterpriseTest : public BrowserWithTestWindowTest {
+ public:
+ SigninUtilEnterpriseTest()
+ : feature_list_(kAccountPoliciesLoadedWithoutSync) {}
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+};
+
+TEST_F(SigninUtilEnterpriseTest, ProfileSeparationEnforcedByPolicy) {
+ std::unique_ptr<TestingProfile> profile = TestingProfile::Builder().Build();
+
+ // No policy set on the active profile.
+ EXPECT_FALSE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(),
+ std::string()));
+ EXPECT_FALSE(
+ signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account_strict"));
+
+ // Active profile has "primary_account" as a user level policy.
+ profile->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account");
+ profile->GetPrefs()->SetBoolean(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine, false);
+ EXPECT_FALSE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(),
+ std::string()));
+ EXPECT_FALSE(
+ signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account_strict"));
+
+ // Active profile has "primary_account_strict" as a user level policy.
+ profile->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account_strict");
+ profile->GetPrefs()->SetBoolean(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine, false);
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(),
+ std::string()));
+ EXPECT_TRUE(
+ signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account_strict"));
+
+ // Active profile has "primary_account" as a machine level policy.
+ profile->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account");
+ profile->GetPrefs()->SetBoolean(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine, true);
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(),
+ std::string()));
+ EXPECT_TRUE(
+ signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account_strict"));
+
+ // Active profile has "primary_account_strict" as a machine level policy.
+ profile->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction,
+ "primary_account");
+ profile->GetPrefs()->SetBoolean(
+ prefs::kManagedAccountsSigninRestrictionScopeMachine, true);
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(),
+ std::string()));
+ EXPECT_TRUE(
+ signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account"));
+ EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(
+ profile.get(), "primary_account_strict"));
+}
+#endif
diff --git a/chromium/chrome/browser/signin/signin_util_win.cc b/chromium/chrome/browser/signin/signin_util_win.cc
new file mode 100644
index 00000000000..0ffc61db1b6
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_util_win.cc
@@ -0,0 +1,332 @@
+// Copyright 2018 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.
+
+#include "chrome/browser/signin/signin_util_win.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/no_destructor.h"
+#include "base/strings/string_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/win/registry.h"
+#include "base/win/win_util.h"
+#include "base/win/wincrypt_shim.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/first_run/first_run.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/profiles/profile_window.h"
+#include "chrome/browser/signin/about_signin_internals_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h"
+#include "chrome/browser/ui/webui/signin/signin_ui_error.h"
+#include "chrome/browser/ui/webui/signin/signin_utils_desktop.h"
+#include "chrome/credential_provider/common/gcp_strings.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/core/browser/about_signin_internals.h"
+#include "components/signin/public/base/signin_metrics.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/accounts_mutator.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+
+namespace signin_util {
+
+namespace {
+
+std::unique_ptr<DiceTurnSyncOnHelper::Delegate>*
+GetDiceTurnSyncOnHelperDelegateForTestingStorage() {
+ static base::NoDestructor<std::unique_ptr<DiceTurnSyncOnHelper::Delegate>>
+ delegate;
+ return delegate.get();
+}
+
+std::string DecryptRefreshToken(const std::string& cipher_text) {
+ DATA_BLOB input;
+ input.pbData =
+ const_cast<BYTE*>(reinterpret_cast<const BYTE*>(cipher_text.data()));
+ input.cbData = static_cast<DWORD>(cipher_text.length());
+ DATA_BLOB output;
+ BOOL result = ::CryptUnprotectData(&input, nullptr, nullptr, nullptr, nullptr,
+ CRYPTPROTECT_UI_FORBIDDEN, &output);
+
+ if (!result)
+ return std::string();
+
+ std::string refresh_token(reinterpret_cast<char*>(output.pbData),
+ output.cbData);
+ ::LocalFree(output.pbData);
+ return refresh_token;
+}
+
+// Finish the process of import credentials. This is either called directly
+// from ImportCredentialsFromProvider() if a browser window for the profile is
+// already available or is delayed until a browser can first be opened.
+void FinishImportCredentialsFromProvider(const CoreAccountId& account_id,
+ Browser* browser,
+ Profile* profile,
+ Profile::CreateStatus status) {
+ // DiceTurnSyncOnHelper deletes itself once done.
+ if (GetDiceTurnSyncOnHelperDelegateForTestingStorage()->get()) {
+ new DiceTurnSyncOnHelper(
+ profile, signin_metrics::AccessPoint::ACCESS_POINT_MACHINE_LOGON,
+ signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT,
+ signin_metrics::Reason::kSigninPrimaryAccount, account_id,
+ DiceTurnSyncOnHelper::SigninAbortedMode::KEEP_ACCOUNT,
+ std::move(*GetDiceTurnSyncOnHelperDelegateForTestingStorage()),
+ base::DoNothing());
+ } else {
+ if (!browser)
+ browser = chrome::FindLastActiveWithProfile(profile);
+
+ new DiceTurnSyncOnHelper(
+ profile, browser,
+ signin_metrics::AccessPoint::ACCESS_POINT_MACHINE_LOGON,
+ signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT,
+ signin_metrics::Reason::kSigninPrimaryAccount, account_id,
+ DiceTurnSyncOnHelper::SigninAbortedMode::KEEP_ACCOUNT);
+ }
+}
+
+// Start the process of importing credentials from the credential provider given
+// that all the required information is available. The process depends on
+// having a browser window for the profile. If a browser window exists the
+// profile be signed in and sync will be starting up. If not, the profile will
+// be still be signed in but sync will be started once the browser window is
+// ready.
+void ImportCredentialsFromProvider(Profile* profile,
+ const std::wstring& gaia_id,
+ const std::wstring& email,
+ const std::string& refresh_token,
+ bool turn_on_sync) {
+ // For debugging purposes, record that the credentials for this profile
+ // came from a credential provider.
+ AboutSigninInternals* signin_internals =
+ AboutSigninInternalsFactory::GetInstance()->GetForProfile(profile);
+ signin_internals->OnAuthenticationResultReceived("Credential Provider");
+
+ CoreAccountId account_id =
+ IdentityManagerFactory::GetForProfile(profile)
+ ->GetAccountsMutator()
+ ->AddOrUpdateAccount(base::WideToUTF8(gaia_id),
+ base::WideToUTF8(email), refresh_token,
+ /*is_under_advanced_protection=*/false,
+ signin_metrics::SourceForRefreshTokenOperation::
+ kMachineLogon_CredentialProvider);
+
+ if (turn_on_sync) {
+ Browser* browser = chrome::FindLastActiveWithProfile(profile);
+ if (browser) {
+ FinishImportCredentialsFromProvider(account_id, browser, profile,
+ Profile::CREATE_STATUS_CREATED);
+ } else {
+ // If no active browser exists yet, this profile is in the process of
+ // being created. Wait for the browser to be created before finishing the
+ // sign in. This object deletes itself when done.
+ new profiles::BrowserAddedForProfileObserver(
+ profile, base::BindRepeating(&FinishImportCredentialsFromProvider,
+ account_id, nullptr));
+ }
+ }
+
+ // Mark this profile as having been signed in with the credential provider.
+ profile->GetPrefs()->SetBoolean(prefs::kSignedInWithCredentialProvider, true);
+}
+
+// Extracts the |cred_provider_gaia_id| and |cred_provider_email| for the user
+// signed in throuhg credential provider.
+void ExtractCredentialProviderUser(std::wstring* cred_provider_gaia_id,
+ std::wstring* cred_provider_email) {
+ DCHECK(cred_provider_gaia_id);
+ DCHECK(cred_provider_email);
+
+ cred_provider_gaia_id->clear();
+ cred_provider_email->clear();
+
+ base::win::RegKey key;
+ if (key.Open(HKEY_CURRENT_USER, credential_provider::kRegHkcuAccountsPath,
+ KEY_READ) != ERROR_SUCCESS) {
+ return;
+ }
+
+ base::win::RegistryKeyIterator it(key.Handle(), L"");
+ if (!it.Valid() || it.SubkeyCount() != 1)
+ return;
+
+ base::win::RegKey key_account(key.Handle(), it.Name(), KEY_QUERY_VALUE);
+ if (!key_account.Valid())
+ return;
+
+ std::wstring email;
+ if (key_account.ReadValue(
+ base::UTF8ToWide(credential_provider::kKeyEmail).c_str(), &email) !=
+ ERROR_SUCCESS) {
+ return;
+ }
+
+ *cred_provider_gaia_id = it.Name();
+ *cred_provider_email = email;
+}
+
+// Attempt to sign in with a credentials from a system installed credential
+// provider if available. If |auth_gaia_id| is not empty then the system
+// credential must be for the same account. Starts the process to turn on DICE
+// only if |turn_on_sync| is true.
+bool TrySigninWithCredentialProvider(Profile* profile,
+ const std::wstring& auth_gaia_id,
+ bool turn_on_sync) {
+ base::win::RegKey key;
+ if (key.Open(HKEY_CURRENT_USER, credential_provider::kRegHkcuAccountsPath,
+ KEY_READ) != ERROR_SUCCESS) {
+ return false;
+ }
+
+ base::win::RegistryKeyIterator it(key.Handle(), L"");
+ if (!it.Valid() || it.SubkeyCount() == 0)
+ return false;
+
+ base::win::RegKey key_account(key.Handle(), it.Name(), KEY_READ | KEY_WRITE);
+ if (!key_account.Valid())
+ return false;
+
+ std::wstring gaia_id = it.Name();
+ if (!auth_gaia_id.empty() && auth_gaia_id != gaia_id)
+ return false;
+
+ std::wstring email;
+ if (key_account.ReadValue(
+ base::UTF8ToWide(credential_provider::kKeyEmail).c_str(), &email) !=
+ ERROR_SUCCESS) {
+ return false;
+ }
+
+ // Read the encrypted refresh token. The data is stored in binary format.
+ // No matter what happens, delete the registry entry.
+
+ std::string encrypted_refresh_token;
+ DWORD size = 0;
+ DWORD type;
+ if (key_account.ReadValue(
+ base::UTF8ToWide(credential_provider::kKeyRefreshToken).c_str(),
+ nullptr, &size, &type) != ERROR_SUCCESS) {
+ return false;
+ }
+
+ encrypted_refresh_token.resize(size);
+ bool reauth_attempted = false;
+ key_account.ReadValue(
+ base::UTF8ToWide(credential_provider::kKeyRefreshToken).c_str(),
+ const_cast<char*>(encrypted_refresh_token.c_str()), &size, &type);
+ if (!gaia_id.empty() && !email.empty() && type == REG_BINARY &&
+ !encrypted_refresh_token.empty()) {
+ std::string refresh_token = DecryptRefreshToken(encrypted_refresh_token);
+ if (!refresh_token.empty()) {
+ reauth_attempted = true;
+ ImportCredentialsFromProvider(profile, gaia_id, email, refresh_token,
+ turn_on_sync);
+ }
+ }
+
+ key_account.DeleteValue(
+ base::UTF8ToWide(credential_provider::kKeyRefreshToken).c_str());
+ return reauth_attempted;
+}
+
+} // namespace
+
+void SetDiceTurnSyncOnHelperDelegateForTesting(
+ std::unique_ptr<DiceTurnSyncOnHelper::Delegate> delegate) {
+ GetDiceTurnSyncOnHelperDelegateForTestingStorage()->swap(delegate);
+}
+
+// Credential provider needs to stick to profile it previously used to import
+// credentials. Thus, if there is another profile that was previously signed in
+// with credential provider regardless of whether user signed in or out,
+// credential provider shouldn't attempt to import credentials into current
+// profile.
+bool IsGCPWUsedInOtherProfile(Profile* profile) {
+ DCHECK(profile);
+
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ if (profile_manager) {
+ std::vector<ProfileAttributesEntry*> entries =
+ profile_manager->GetProfileAttributesStorage()
+ .GetAllProfilesAttributes();
+
+ for (const ProfileAttributesEntry* entry : entries) {
+ if (entry->GetPath() == profile->GetPath())
+ continue;
+
+ if (entry->IsSignedInWithCredentialProvider())
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void SigninWithCredentialProviderIfPossible(Profile* profile) {
+ // This flow is used for first time signin through credential provider. Any
+ // subsequent signin for the credential provider user needs to go through
+ // reauth flow.
+ if (profile->GetPrefs()->GetBoolean(prefs::kSignedInWithCredentialProvider))
+ return;
+
+ std::wstring cred_provider_gaia_id;
+ std::wstring cred_provider_email;
+
+ ExtractCredentialProviderUser(&cred_provider_gaia_id, &cred_provider_email);
+ if (cred_provider_gaia_id.empty() || cred_provider_email.empty())
+ return;
+
+ // Chrome doesn't allow signing into current profile if the same user is
+ // signed in another profile.
+ if (!CanOfferSignin(profile, base::WideToUTF8(cred_provider_gaia_id),
+ base::WideToUTF8(cred_provider_email))
+ .IsOk() ||
+ IsGCPWUsedInOtherProfile(profile)) {
+ return;
+ }
+
+ auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
+ std::wstring gaia_id;
+ if (identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) {
+ gaia_id = base::UTF8ToWide(
+ identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSync)
+ .gaia);
+ }
+
+ TrySigninWithCredentialProvider(profile, gaia_id, gaia_id.empty());
+}
+
+bool ReauthWithCredentialProviderIfPossible(Profile* profile) {
+ // Check to see if auto signin information is available. Only applies if:
+ //
+ // - The profile is marked as having been signed in with a system credential.
+ // - The profile is already signed in.
+ // - The profile is in an auth error state.
+ auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
+ if (!(profile->GetPrefs()->GetBoolean(
+ prefs::kSignedInWithCredentialProvider) &&
+ identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync) &&
+ identity_manager->HasAccountWithRefreshTokenInPersistentErrorState(
+ identity_manager->GetPrimaryAccountId(
+ signin::ConsentLevel::kSync)))) {
+ return false;
+ }
+
+ std::wstring gaia_id = base::UTF8ToWide(
+ identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSync)
+ .gaia.c_str());
+ return TrySigninWithCredentialProvider(profile, gaia_id, false);
+}
+
+} // namespace signin_util
diff --git a/chromium/chrome/browser/signin/signin_util_win.h b/chromium/chrome/browser/signin/signin_util_win.h
new file mode 100644
index 00000000000..771085f9bbb
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_util_win.h
@@ -0,0 +1,31 @@
+// Copyright 2018 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_SIGNIN_SIGNIN_UTIL_WIN_H_
+#define CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_WIN_H_
+
+#include <memory>
+
+#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h"
+
+class Profile;
+
+namespace signin_util {
+
+// Attempt to sign in with a credentials from a system installed credential
+// provider if available.
+void SigninWithCredentialProviderIfPossible(Profile* profile);
+
+// Attempt to reauthenticate with a credentials from a system installed
+// credential provider if available. If a new authentication token was
+// installed returns true.
+bool ReauthWithCredentialProviderIfPossible(Profile* profile);
+
+// Sets the DiceTurnSyncOnHelper delegate for browser tests.
+void SetDiceTurnSyncOnHelperDelegateForTesting(
+ std::unique_ptr<DiceTurnSyncOnHelper::Delegate> delegate);
+
+} // namespace signin_util
+
+#endif // CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_WIN_H_
diff --git a/chromium/chrome/browser/signin/signin_util_win_browsertest.cc b/chromium/chrome/browser/signin/signin_util_win_browsertest.cc
new file mode 100644
index 00000000000..87832ab6e98
--- /dev/null
+++ b/chromium/chrome/browser/signin/signin_util_win_browsertest.cc
@@ -0,0 +1,698 @@
+// Copyright 2018 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.
+
+#include <stddef.h>
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/command_line.h"
+#include "base/run_loop.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/test/bind.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/test/test_reg_util_win.h"
+#include "base/win/wincrypt_shim.h"
+#include "build/build_config.h"
+#include "chrome/browser/first_run/first_run.h"
+#include "chrome/browser/profiles/profile_attributes_entry.h"
+#include "chrome/browser/profiles/profile_attributes_storage.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/profiles/profile_window.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/browser/signin/signin_util_win.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/ui_features.h"
+#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/credential_provider/common/gcp_strings.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "components/prefs/pref_service.h"
+#include "components/signin/public/base/signin_pref_names.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "components/signin/public/identity_manager/primary_account_mutator.h"
+#include "content/public/test/browser_test.h"
+
+class SigninUIError;
+
+namespace {
+
+class TestDiceTurnSyncOnHelperDelegate : public DiceTurnSyncOnHelper::Delegate {
+ ~TestDiceTurnSyncOnHelperDelegate() override {}
+
+ // DiceTurnSyncOnHelper::Delegate:
+ void ShowLoginError(const SigninUIError& error) override {}
+ void ShowMergeSyncDataConfirmation(
+ const std::string& previous_email,
+ const std::string& new_email,
+ DiceTurnSyncOnHelper::SigninChoiceCallback callback) override {
+ std::move(callback).Run(DiceTurnSyncOnHelper::SIGNIN_CHOICE_CONTINUE);
+ }
+ void ShowEnterpriseAccountConfirmation(
+ const AccountInfo& account_info,
+ DiceTurnSyncOnHelper::SigninChoiceCallback callback) override {
+ std::move(callback).Run(DiceTurnSyncOnHelper::SIGNIN_CHOICE_CONTINUE);
+ }
+ void ShowSyncConfirmation(
+ base::OnceCallback<void(LoginUIService::SyncConfirmationUIClosedResult)>
+ callback) override {
+ std::move(callback).Run(LoginUIService::SYNC_WITH_DEFAULT_SETTINGS);
+ }
+ void ShowSyncDisabledConfirmation(
+ bool is_managed_account,
+ base::OnceCallback<void(LoginUIService::SyncConfirmationUIClosedResult)>
+ callback) override {}
+ void ShowSyncSettings() override {}
+ void SwitchToProfile(Profile* new_profile) override {}
+};
+
+struct SigninUtilWinBrowserTestParams {
+ SigninUtilWinBrowserTestParams(bool is_first_run,
+ const std::wstring& gaia_id,
+ const std::wstring& email,
+ const std::string& refresh_token,
+ bool expect_is_started)
+ : is_first_run(is_first_run),
+ gaia_id(gaia_id),
+ email(email),
+ refresh_token(refresh_token),
+ expect_is_started(expect_is_started) {}
+
+ bool is_first_run = false;
+ std::wstring gaia_id;
+ std::wstring email;
+ std::string refresh_token;
+ bool expect_is_started = false;
+};
+
+void AssertSigninStarted(bool expect_is_started, Profile* profile) {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+
+ ProfileAttributesStorage& storage =
+ profile_manager->GetProfileAttributesStorage();
+
+ ProfileAttributesEntry* entry =
+ storage.GetProfileAttributesWithPath(profile->GetPath());
+
+ ASSERT_NE(entry, nullptr);
+
+ ASSERT_EQ(expect_is_started, entry->IsSignedInWithCredentialProvider());
+}
+
+} // namespace
+
+class BrowserTestHelper {
+ public:
+ BrowserTestHelper(const std::wstring& gaia_id,
+ const std::wstring& email,
+ const std::string& refresh_token)
+ : gaia_id_(gaia_id), email_(email), refresh_token_(refresh_token) {}
+
+ protected:
+ void CreateRegKey(base::win::RegKey* key) {
+ if (!gaia_id_.empty()) {
+ EXPECT_EQ(
+ ERROR_SUCCESS,
+ key->Create(HKEY_CURRENT_USER,
+ credential_provider::kRegHkcuAccountsPath, KEY_WRITE));
+ EXPECT_EQ(ERROR_SUCCESS, key->CreateKey(gaia_id_.c_str(), KEY_WRITE));
+ }
+ }
+
+ void WriteRefreshToken(base::win::RegKey* key,
+ const std::string& refresh_token) {
+ EXPECT_TRUE(key->Valid());
+ DATA_BLOB plaintext;
+ plaintext.pbData =
+ reinterpret_cast<BYTE*>(const_cast<char*>(refresh_token.c_str()));
+ plaintext.cbData = static_cast<DWORD>(refresh_token.length());
+
+ DATA_BLOB ciphertext;
+ ASSERT_TRUE(::CryptProtectData(&plaintext, L"Gaia refresh token", nullptr,
+ nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN,
+ &ciphertext));
+ std::string encrypted_data(reinterpret_cast<char*>(ciphertext.pbData),
+ ciphertext.cbData);
+ EXPECT_EQ(
+ ERROR_SUCCESS,
+ key->WriteValue(
+ base::ASCIIToWide(credential_provider::kKeyRefreshToken).c_str(),
+ encrypted_data.c_str(), encrypted_data.length(), REG_BINARY));
+ LocalFree(ciphertext.pbData);
+ }
+
+ void ExpectRefreshTokenExists(bool exists) {
+ base::win::RegKey key;
+ EXPECT_EQ(ERROR_SUCCESS,
+ key.Open(HKEY_CURRENT_USER,
+ credential_provider::kRegHkcuAccountsPath, KEY_READ));
+ EXPECT_EQ(ERROR_SUCCESS, key.OpenKey(gaia_id_.c_str(), KEY_READ));
+ EXPECT_EQ(
+ exists,
+ key.HasValue(
+ base::ASCIIToWide(credential_provider::kKeyRefreshToken).c_str()));
+ }
+
+ public:
+ void SetSigninUtilRegistry() {
+ base::win::RegKey key;
+ CreateRegKey(&key);
+
+ if (!email_.empty()) {
+ EXPECT_TRUE(key.Valid());
+ EXPECT_EQ(ERROR_SUCCESS,
+ key.WriteValue(
+ base::ASCIIToWide(credential_provider::kKeyEmail).c_str(),
+ email_.c_str()));
+ }
+
+ if (!refresh_token_.empty())
+ WriteRefreshToken(&key, refresh_token_);
+ }
+
+ bool IsPreTest() {
+ std::string test_name =
+ ::testing::UnitTest::GetInstance()->current_test_info()->name();
+ LOG(INFO) << "PRE_ test_name " << test_name;
+ return test_name.find("PRE_") != std::string::npos;
+ }
+
+ bool IsPrePreTest() {
+ std::string test_name =
+ ::testing::UnitTest::GetInstance()->current_test_info()->name();
+ LOG(INFO) << "PRE_PRE_ test_name " << test_name;
+ return test_name.find("PRE_PRE_") != std::string::npos;
+ }
+
+ private:
+ std::wstring gaia_id_;
+ std::wstring email_;
+ std::string refresh_token_;
+};
+
+class SigninUtilWinBrowserTest
+ : public BrowserTestHelper,
+ public InProcessBrowserTest,
+ public testing::WithParamInterface<SigninUtilWinBrowserTestParams> {
+ public:
+ SigninUtilWinBrowserTest()
+ : BrowserTestHelper(GetParam().gaia_id,
+ GetParam().email,
+ GetParam().refresh_token) {}
+
+ protected:
+ void SetUpCommandLine(base::CommandLine* command_line) override {
+ command_line->AppendSwitch(GetParam().is_first_run
+ ? switches::kForceFirstRun
+ : switches::kNoFirstRun);
+ }
+
+ bool SetUpUserDataDirectory() override {
+ registry_override_.OverrideRegistry(HKEY_CURRENT_USER);
+
+ signin_util::SetDiceTurnSyncOnHelperDelegateForTesting(
+ std::unique_ptr<DiceTurnSyncOnHelper::Delegate>(
+ new TestDiceTurnSyncOnHelperDelegate()));
+
+ SetSigninUtilRegistry();
+
+ return InProcessBrowserTest::SetUpUserDataDirectory();
+ }
+
+ private:
+ registry_util::RegistryOverrideManager registry_override_;
+};
+
+IN_PROC_BROWSER_TEST_P(SigninUtilWinBrowserTest, Run) {
+ ASSERT_EQ(GetParam().is_first_run, first_run::IsChromeFirstRun());
+
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles());
+
+ Profile* profile = profile_manager->GetLastUsedProfile();
+ ASSERT_EQ(profile_manager->GetInitialProfileDir(), profile->GetBaseName());
+
+ Browser* browser = chrome::FindLastActiveWithProfile(profile);
+ ASSERT_NE(nullptr, browser);
+
+ AssertSigninStarted(GetParam().expect_is_started, profile);
+
+ // If a refresh token was specified and a sign in attempt was expected, make
+ // sure the refresh token was removed from the registry.
+ if (!GetParam().refresh_token.empty() && GetParam().expect_is_started)
+ ExpectRefreshTokenExists(false);
+}
+
+IN_PROC_BROWSER_TEST_P(SigninUtilWinBrowserTest, ReauthNoop) {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles());
+
+ Profile* profile = profile_manager->GetLastUsedProfile();
+
+ // Whether the profile was signed in with the credential provider or not,
+ // reauth should be a noop.
+ ASSERT_FALSE(signin_util::ReauthWithCredentialProviderIfPossible(profile));
+}
+
+IN_PROC_BROWSER_TEST_P(SigninUtilWinBrowserTest, NoReauthAfterSignout) {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles());
+
+ Profile* profile = profile_manager->GetLastUsedProfile();
+
+ if (GetParam().expect_is_started) {
+ // Write a new refresh token.
+ base::win::RegKey key;
+ CreateRegKey(&key);
+ WriteRefreshToken(&key, "lst-new");
+ ASSERT_FALSE(signin_util::ReauthWithCredentialProviderIfPossible(profile));
+
+ // Sign user out of browser.
+ auto* primary_account_mutator =
+ IdentityManagerFactory::GetForProfile(profile)
+ ->GetPrimaryAccountMutator();
+ primary_account_mutator->RevokeSyncConsent(
+ signin_metrics::FORCE_SIGNOUT_ALWAYS_ALLOWED_FOR_TEST,
+ signin_metrics::SignoutDelete::kDeleted);
+
+ // Even with a refresh token available, no reauth happens if the profile
+ // is signed out.
+ ASSERT_FALSE(signin_util::ReauthWithCredentialProviderIfPossible(profile));
+ }
+}
+
+IN_PROC_BROWSER_TEST_P(SigninUtilWinBrowserTest, FixReauth) {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+
+ ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles());
+
+ Profile* profile = profile_manager->GetLastUsedProfile();
+
+ if (GetParam().expect_is_started) {
+ // Write a new refresh token. This time reauth should work.
+ base::win::RegKey key;
+ CreateRegKey(&key);
+ WriteRefreshToken(&key, "lst-new");
+ ASSERT_FALSE(signin_util::ReauthWithCredentialProviderIfPossible(profile));
+
+ // Make sure the profile stays signed in, but in an auth error state.
+ auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
+ signin::UpdatePersistentErrorOfRefreshTokenForAccount(
+ identity_manager,
+ identity_manager->GetPrimaryAccountId(signin::ConsentLevel::kSync),
+ GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
+ GoogleServiceAuthError::InvalidGaiaCredentialsReason::
+ CREDENTIALS_REJECTED_BY_SERVER));
+
+ // If the profile remains signed in but is in an auth error state,
+ // reauth should happen.
+ ASSERT_TRUE(signin_util::ReauthWithCredentialProviderIfPossible(profile));
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest1,
+ SigninUtilWinBrowserTest,
+ testing::Values(SigninUtilWinBrowserTestParams(
+ /*is_first_run=*/false,
+ /*gaia_id=*/std::wstring(),
+ /*email=*/std::wstring(),
+ /*refresh_token=*/std::string(),
+ /*expect_is_started=*/false)));
+
+INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest2,
+ SigninUtilWinBrowserTest,
+ testing::Values(SigninUtilWinBrowserTestParams(
+ /*is_first_run=*/true,
+ /*gaia_id=*/std::wstring(),
+ /*email=*/std::wstring(),
+ /*refresh_token=*/std::string(),
+ /*expect_is_started=*/false)));
+
+INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest3,
+ SigninUtilWinBrowserTest,
+ testing::Values(SigninUtilWinBrowserTestParams(
+ /*is_first_run=*/true,
+ /*gaia_id=*/L"gaia-123456",
+ /*email=*/std::wstring(),
+ /*refresh_token=*/std::string(),
+ /*expect_is_started=*/false)));
+
+INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest4,
+ SigninUtilWinBrowserTest,
+ testing::Values(SigninUtilWinBrowserTestParams(
+ /*is_first_run=*/true,
+ /*gaia_id=*/L"gaia-123456",
+ /*email=*/L"foo@gmail.com",
+ /*refresh_token=*/std::string(),
+ /*expect_is_started=*/false)));
+
+INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest5,
+ SigninUtilWinBrowserTest,
+ testing::Values(SigninUtilWinBrowserTestParams(
+ /*is_first_run=*/true,
+ /*gaia_id=*/L"gaia-123456",
+ /*email=*/L"foo@gmail.com",
+ /*refresh_token=*/"lst-123456",
+ /*expect_is_started=*/true)));
+
+INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest6,
+ SigninUtilWinBrowserTest,
+ testing::Values(SigninUtilWinBrowserTestParams(
+ /*is_first_run=*/false,
+ /*gaia_id=*/L"gaia-123456",
+ /*email=*/L"foo@gmail.com",
+ /*refresh_token=*/"lst-123456",
+ /*expect_is_started=*/true)));
+
+struct ExistingWinBrowserSigninUtilTestParams : SigninUtilWinBrowserTestParams {
+ ExistingWinBrowserSigninUtilTestParams(
+ const std::wstring& gaia_id,
+ const std::wstring& email,
+ const std::string& refresh_token,
+ const std::wstring& existing_email,
+ bool expect_is_started)
+ : SigninUtilWinBrowserTestParams(false,
+ gaia_id,
+ email,
+ refresh_token,
+ expect_is_started),
+ existing_email(existing_email) {}
+
+ std::wstring existing_email;
+};
+
+class ExistingWinBrowserSigninUtilTest
+ : public BrowserTestHelper,
+ public InProcessBrowserTest,
+ public testing::WithParamInterface<
+ ExistingWinBrowserSigninUtilTestParams> {
+ public:
+ ExistingWinBrowserSigninUtilTest()
+ : BrowserTestHelper(GetParam().gaia_id,
+ GetParam().email,
+ GetParam().refresh_token) {}
+
+ protected:
+ bool SetUpUserDataDirectory() override {
+ registry_override_.OverrideRegistry(HKEY_CURRENT_USER);
+
+ signin_util::SetDiceTurnSyncOnHelperDelegateForTesting(
+ std::unique_ptr<DiceTurnSyncOnHelper::Delegate>(
+ new TestDiceTurnSyncOnHelperDelegate()));
+ if (!IsPreTest())
+ SetSigninUtilRegistry();
+
+ return InProcessBrowserTest::SetUpUserDataDirectory();
+ }
+
+ private:
+ registry_util::RegistryOverrideManager registry_override_;
+};
+
+IN_PROC_BROWSER_TEST_P(ExistingWinBrowserSigninUtilTest,
+ PRE_ExistingWinBrowser) {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ Profile* profile = profile_manager->GetLastUsedProfile();
+
+ ASSERT_EQ(profile_manager->GetInitialProfileDir(), profile->GetBaseName());
+
+ if (!GetParam().existing_email.empty()) {
+ auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
+
+ ASSERT_TRUE(identity_manager);
+
+ signin::MakePrimaryAccountAvailable(
+ identity_manager, base::WideToUTF8(GetParam().existing_email),
+ signin::ConsentLevel::kSync);
+
+ ASSERT_TRUE(
+ identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ }
+}
+
+IN_PROC_BROWSER_TEST_P(ExistingWinBrowserSigninUtilTest, ExistingWinBrowser) {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles());
+
+ Profile* profile = profile_manager->GetLastUsedProfile();
+ ASSERT_EQ(profile_manager->GetInitialProfileDir(), profile->GetBaseName());
+
+ AssertSigninStarted(GetParam().expect_is_started, profile);
+
+ // If a refresh token was specified and a sign in attempt was expected, make
+ // sure the refresh token was removed from the registry.
+ if (!GetParam().refresh_token.empty() && GetParam().expect_is_started)
+ ExpectRefreshTokenExists(false);
+}
+
+INSTANTIATE_TEST_SUITE_P(AllowSubsequentRun,
+ ExistingWinBrowserSigninUtilTest,
+ testing::Values(ExistingWinBrowserSigninUtilTestParams(
+ /*gaia_id=*/L"gaia-123456",
+ /*email=*/L"foo@gmail.com",
+ /*refresh_token=*/"lst-123456",
+ /*existing_email=*/std::wstring(),
+ /*expect_is_started=*/true)));
+
+INSTANTIATE_TEST_SUITE_P(OnlyAllowProfileWithNoPrimaryAccount,
+ ExistingWinBrowserSigninUtilTest,
+ testing::Values(ExistingWinBrowserSigninUtilTestParams(
+ /*gaia_id=*/L"gaia_id_for_foo_gmail.com",
+ /*email=*/L"foo@gmail.com",
+ /*refresh_token=*/"lst-123456",
+ /*existing_email=*/L"bar@gmail.com",
+ /*expect_is_started=*/false)));
+
+INSTANTIATE_TEST_SUITE_P(AllowProfileWithPrimaryAccount_DifferentUser,
+ ExistingWinBrowserSigninUtilTest,
+ testing::Values(ExistingWinBrowserSigninUtilTestParams(
+ /*gaia_id=*/L"gaia_id_for_foo_gmail.com",
+ /*email=*/L"foo@gmail.com",
+ /*refresh_token=*/"lst-123456",
+ /*existing_email=*/L"bar@gmail.com",
+ /*expect_is_started=*/false)));
+
+
+INSTANTIATE_TEST_SUITE_P(AllowProfileWithPrimaryAccount_SameUser,
+ ExistingWinBrowserSigninUtilTest,
+ testing::Values(ExistingWinBrowserSigninUtilTestParams(
+ /*gaia_id=*/L"gaia_id_for_foo_gmail.com",
+ /*email=*/L"foo@gmail.com",
+ /*refresh_token=*/"lst-123456",
+ /*existing_email=*/L"foo@gmail.com",
+ /*expect_is_started=*/true)));
+
+void UnblockOnProfileInitialized(base::OnceClosure quit_closure,
+ Profile* profile,
+ Profile::CreateStatus status) {
+ // If the status is CREATE_STATUS_CREATED, then the function will be called
+ // again with CREATE_STATUS_INITIALIZED.
+ if (status == Profile::CREATE_STATUS_CREATED)
+ return;
+
+ EXPECT_EQ(Profile::CREATE_STATUS_INITIALIZED, status);
+ std::move(quit_closure).Run();
+}
+
+void CreateAndSwitchToProfile(const std::string& basepath) {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ ASSERT_TRUE(profile_manager);
+
+ base::FilePath path = profile_manager->user_data_dir().AppendASCII(basepath);
+ base::RunLoop run_loop;
+ profile_manager->CreateProfileAsync(
+ path, base::BindRepeating(&UnblockOnProfileInitialized,
+ run_loop.QuitClosure()));
+ // Run the message loop to allow profile initialization to take place; the
+ // loop is terminated by UnblockOnProfileInitialized.
+ run_loop.Run();
+
+ profiles::SwitchToProfile(path, false, ProfileManager::CreateCallback());
+}
+
+struct ExistingWinBrowserProfilesSigninUtilTestParams {
+ ExistingWinBrowserProfilesSigninUtilTestParams(
+ const std::wstring& email_in_other_profile,
+ bool cred_provider_used_other_profile,
+ const std::wstring& current_profile,
+ const std::wstring& email_in_current_profile,
+ bool expect_is_started)
+ : email_in_other_profile(email_in_other_profile),
+ cred_provider_used_other_profile(cred_provider_used_other_profile),
+ current_profile(current_profile),
+ email_in_current_profile(email_in_current_profile),
+ expect_is_started(expect_is_started) {}
+
+ std::wstring email_in_other_profile;
+ bool cred_provider_used_other_profile;
+ std::wstring current_profile;
+ std::wstring email_in_current_profile;
+ bool expect_is_started;
+};
+
+class ExistingWinBrowserProfilesSigninUtilTest
+ : public BrowserTestHelper,
+ public InProcessBrowserTest,
+ public testing::WithParamInterface<
+ ExistingWinBrowserProfilesSigninUtilTestParams> {
+ public:
+ ExistingWinBrowserProfilesSigninUtilTest()
+ : BrowserTestHelper(L"gaia_id_for_foo_gmail.com",
+ L"foo@gmail.com",
+ "lst-123456") {}
+
+ protected:
+ bool SetUpUserDataDirectory() override {
+ registry_override_.OverrideRegistry(HKEY_CURRENT_USER);
+
+ signin_util::SetDiceTurnSyncOnHelperDelegateForTesting(
+ std::unique_ptr<DiceTurnSyncOnHelper::Delegate>(
+ new TestDiceTurnSyncOnHelperDelegate()));
+ if (!IsPreTest()) {
+ SetSigninUtilRegistry();
+ } else if (IsPrePreTest() && GetParam().cred_provider_used_other_profile) {
+ BrowserTestHelper(L"gaia_id_for_bar_gmail.com", L"bar@gmail.com",
+ "lst-123456")
+ .SetSigninUtilRegistry();
+ }
+
+ return InProcessBrowserTest::SetUpUserDataDirectory();
+ }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+ registry_util::RegistryOverrideManager registry_override_;
+};
+
+// In PRE_PRE_Run, browser starts for the first time with the initial profile
+// dir. If needed by the test, this step can set |email_in_other_profile| as the
+// primary account in the profile or it can sign in with credential provider,
+// but before this step ends, |current_profile| is created and browser switches
+// to that profile just to prepare the browser for the next step.
+IN_PROC_BROWSER_TEST_P(ExistingWinBrowserProfilesSigninUtilTest, PRE_PRE_Run) {
+ g_browser_process->local_state()->SetBoolean(
+ prefs::kBrowserShowProfilePickerOnStartup, false);
+
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+
+ Profile* profile = profile_manager->GetLastUsedProfile();
+
+ ASSERT_EQ(profile_manager->GetInitialProfileDir(), profile->GetBaseName());
+
+ auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
+ ASSERT_TRUE(identity_manager);
+ ASSERT_TRUE(
+ identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync) ==
+ GetParam().cred_provider_used_other_profile);
+
+ if (!GetParam().cred_provider_used_other_profile &&
+ !GetParam().email_in_other_profile.empty()) {
+ signin::MakePrimaryAccountAvailable(
+ identity_manager, base::WideToUTF8(GetParam().email_in_other_profile),
+ signin::ConsentLevel::kSync);
+
+ ASSERT_TRUE(
+ identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ }
+
+ CreateAndSwitchToProfile(base::WideToUTF8(GetParam().current_profile));
+}
+
+// Browser starts with the |current_profile| profile created in the previous
+// step. If needed by the test, this step can set |email_in_current_profile| as
+// the primary account in the profile.
+IN_PROC_BROWSER_TEST_P(ExistingWinBrowserProfilesSigninUtilTest, PRE_Run) {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ Profile* profile = profile_manager->GetLastUsedProfile();
+
+ ASSERT_EQ(GetParam().current_profile, profile->GetBaseName().value());
+
+ auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
+ ASSERT_TRUE(identity_manager);
+ ASSERT_FALSE(
+ identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync));
+
+ if (!GetParam().email_in_current_profile.empty()) {
+ signin::MakePrimaryAccountAvailable(
+ identity_manager, base::WideToUTF8(GetParam().email_in_current_profile),
+ signin::ConsentLevel::kSync);
+
+ ASSERT_TRUE(
+ identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync));
+ }
+}
+
+// Before this step runs, refresh token is written into fake registry. Browser
+// starts with the |current_profile| profile. Depending on the test case,
+// profile may have a primary account. Similarly the other profile(initial
+// profile in this case) may have a primary account as well.
+IN_PROC_BROWSER_TEST_P(ExistingWinBrowserProfilesSigninUtilTest, Run) {
+ ProfileManager* profile_manager = g_browser_process->profile_manager();
+ Profile* profile = profile_manager->GetLastUsedProfile();
+
+ ASSERT_EQ(GetParam().current_profile, profile->GetBaseName().value());
+ AssertSigninStarted(GetParam().expect_is_started, profile);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ AllowCurrentProfile_NoUserSignedIn,
+ ExistingWinBrowserProfilesSigninUtilTest,
+ testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams(
+ /*email_in_other_profile*/ L"",
+ /*cred_provider_used_other_profile*/ false,
+ /*current_profile*/ L"profile1",
+ /*email_in_current_profile=*/L"",
+ /*expect_is_started=*/true)));
+
+INSTANTIATE_TEST_SUITE_P(
+ AllowCurrentProfile_SameUserSignedIn,
+ ExistingWinBrowserProfilesSigninUtilTest,
+ testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams(
+ /*email_in_other_profile*/ L"",
+ /*cred_provider_used_other_profile*/ false,
+ /*current_profile*/ L"profile1",
+ /*email_in_current_profile=*/L"foo@gmail.com",
+ /*expect_is_started=*/true)));
+
+INSTANTIATE_TEST_SUITE_P(
+ DisallowCurrentProfile_DifferentUserSignedIn,
+ ExistingWinBrowserProfilesSigninUtilTest,
+ testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams(
+ /*email_in_other_profile*/ L"",
+ /*cred_provider_used_other_profile*/ false,
+ /*current_profile*/ L"profile1",
+ /*email_in_current_profile=*/L"bar@gmail.com",
+ /*expect_is_started=*/false)));
+
+INSTANTIATE_TEST_SUITE_P(
+ DisallowCurrentProfile_SameUserSignedInDefaultProfile,
+ ExistingWinBrowserProfilesSigninUtilTest,
+ testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams(
+ /*email_in_other_profile*/ L"foo@gmail.com",
+ /*cred_provider_used_other_profile*/ false,
+ /*current_profile*/ L"profile1",
+ /*email_in_current_profile=*/L"",
+ /*expect_is_started=*/false)));
+
+INSTANTIATE_TEST_SUITE_P(
+ AllowCurrentProfile_DifferentUserSignedInDefaultProfile,
+ ExistingWinBrowserProfilesSigninUtilTest,
+ testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams(
+ /*email_in_other_profile*/ L"bar@gmail.com",
+ /*cred_provider_used_other_profile*/ false,
+ /*current_profile*/ L"profile1",
+ /*email_in_current_profile=*/L"",
+ /*expect_is_started=*/true)));
+
+INSTANTIATE_TEST_SUITE_P(
+ DisallowCurrentProfile_CredProviderUsedDefaultProfile,
+ ExistingWinBrowserProfilesSigninUtilTest,
+ testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams(
+ /*email_in_other_profile*/ L"",
+ /*cred_provider_used_other_profile*/ true,
+ /*current_profile*/ L"profile1",
+ /*email_in_current_profile=*/L"",
+ /*expect_is_started=*/false)));
diff --git a/chromium/chrome/browser/signin/test_signin_client_builder.cc b/chromium/chrome/browser/signin/test_signin_client_builder.cc
new file mode 100644
index 00000000000..9795a5e7e85
--- /dev/null
+++ b/chromium/chrome/browser/signin/test_signin_client_builder.cc
@@ -0,0 +1,18 @@
+// Copyright 2014 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.
+
+#include "chrome/browser/signin/test_signin_client_builder.h"
+
+#include "chrome/browser/profiles/profile.h"
+#include "components/signin/public/base/test_signin_client.h"
+
+namespace signin {
+
+std::unique_ptr<KeyedService> BuildTestSigninClient(
+ content::BrowserContext* context) {
+ return std::make_unique<TestSigninClient>(
+ static_cast<Profile*>(context)->GetPrefs());
+}
+
+} // namespace signin
diff --git a/chromium/chrome/browser/signin/test_signin_client_builder.h b/chromium/chrome/browser/signin/test_signin_client_builder.h
new file mode 100644
index 00000000000..3863c510150
--- /dev/null
+++ b/chromium/chrome/browser/signin/test_signin_client_builder.h
@@ -0,0 +1,26 @@
+// Copyright 2014 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_SIGNIN_TEST_SIGNIN_CLIENT_BUILDER_H_
+#define CHROME_BROWSER_SIGNIN_TEST_SIGNIN_CLIENT_BUILDER_H_
+
+#include <memory>
+
+class KeyedService;
+
+namespace content {
+class BrowserContext;
+}
+
+namespace signin {
+
+// Method to be used by the |ChromeSigninClientFactory| to create a test version
+// of the SigninClient
+std::unique_ptr<KeyedService> BuildTestSigninClient(
+ content::BrowserContext* context);
+
+} // namespace signin
+
+
+#endif // CHROME_BROWSER_SIGNIN_TEST_SIGNIN_CLIENT_BUILDER_H_
diff --git a/chromium/chrome/browser/signin/token_revoker_test_utils.cc b/chromium/chrome/browser/signin/token_revoker_test_utils.cc
new file mode 100644
index 00000000000..69b26c881b1
--- /dev/null
+++ b/chromium/chrome/browser/signin/token_revoker_test_utils.cc
@@ -0,0 +1,36 @@
+// Copyright 2016 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.
+
+#include "chrome/browser/signin/token_revoker_test_utils.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/net/system_network_context_manager.h"
+#include "content/public/test/test_utils.h"
+#include "google_apis/gaia/gaia_auth_fetcher.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+
+namespace token_revoker_test_utils {
+
+RefreshTokenRevoker::RefreshTokenRevoker()
+ : gaia_fetcher_(this,
+ gaia::GaiaSource::kChrome,
+ g_browser_process->system_network_context_manager()
+ ->GetSharedURLLoaderFactory()) {}
+
+RefreshTokenRevoker::~RefreshTokenRevoker() {
+}
+
+void RefreshTokenRevoker::Revoke(const std::string& token) {
+ DVLOG(1) << "Starting RefreshTokenRevoker for token: " << token;
+ gaia_fetcher_.StartRevokeOAuth2Token(token);
+ message_loop_runner_ = new content::MessageLoopRunner;
+ message_loop_runner_->Run();
+}
+
+void RefreshTokenRevoker::OnOAuth2RevokeTokenCompleted(
+ GaiaAuthConsumer::TokenRevocationStatus status) {
+ DVLOG(1) << "TokenRevoker OnOAuth2RevokeTokenCompleted";
+ message_loop_runner_->Quit();
+}
+
+} // namespace token_revoker_test_utils
diff --git a/chromium/chrome/browser/signin/token_revoker_test_utils.h b/chromium/chrome/browser/signin/token_revoker_test_utils.h
new file mode 100644
index 00000000000..2c1b08a257a
--- /dev/null
+++ b/chromium/chrome/browser/signin/token_revoker_test_utils.h
@@ -0,0 +1,42 @@
+// Copyright 2016 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_SIGNIN_TOKEN_REVOKER_TEST_UTILS_H_
+#define CHROME_BROWSER_SIGNIN_TOKEN_REVOKER_TEST_UTILS_H_
+
+#include "base/memory/ref_counted.h"
+#include "google_apis/gaia/gaia_auth_fetcher.h"
+
+namespace content {
+class MessageLoopRunner;
+}
+
+namespace token_revoker_test_utils {
+
+// A helper class that takes care of asynchronously revoking a refresh token.
+class RefreshTokenRevoker : public GaiaAuthConsumer {
+ public:
+ RefreshTokenRevoker();
+
+ RefreshTokenRevoker(const RefreshTokenRevoker&) = delete;
+ RefreshTokenRevoker& operator=(const RefreshTokenRevoker&) = delete;
+
+ ~RefreshTokenRevoker() override;
+
+ // Sends a request to Gaia servers to revoke the refresh token. Blocks until
+ // it is revoked, i.e. until OnOAuth2RevokeTokenCompleted is fired.
+ void Revoke(const std::string& token);
+
+ // Called when token is revoked.
+ void OnOAuth2RevokeTokenCompleted(
+ GaiaAuthConsumer::TokenRevocationStatus status) override;
+
+ private:
+ GaiaAuthFetcher gaia_fetcher_;
+ scoped_refptr<content::MessageLoopRunner> message_loop_runner_;
+};
+
+} // namespace token_revoker_test_utils
+
+#endif // CHROME_BROWSER_SIGNIN_TOKEN_REVOKER_TEST_UTILS_H_
diff --git a/chromium/components/gcm_driver/DEPS b/chromium/components/gcm_driver/DEPS
new file mode 100644
index 00000000000..45c75405fd0
--- /dev/null
+++ b/chromium/components/gcm_driver/DEPS
@@ -0,0 +1,27 @@
+include_rules = [
+ "+components/crx_file/id_util.h",
+ "+components/os_crypt",
+ "+components/keyed_service",
+ "+components/pref_registry",
+ "+components/prefs",
+ "+components/signin/public",
+ "+components/version_info",
+ "+content/public/browser/browser_context.h",
+ "+content/public/test", # Only used for tests.
+ "+crypto", # Only used for tests.
+
+ # Whitelist specific headers from //google_apis/gaia. Contact
+ # blundell@chromium.org if looking to add more.
+ "+google_apis/gaia/core_account_id.h",
+ "+google_apis/gaia/gaia_constants.h",
+ "+google_apis/gaia/gaia_oauth_client.h",
+ "+google_apis/gaia/google_service_auth_error.h",
+ "+google_apis/gcm",
+ "+mojo/public",
+ "+net",
+ "+services/network/public/cpp",
+ "+services/network/public/mojom",
+ "+services/network/test",
+ "+third_party/leveldatabase", # Only used for tests.
+ "+third_party/re2", # Only used for tests.
+]
diff --git a/chromium/components/gcm_driver/DIR_METADATA b/chromium/components/gcm_driver/DIR_METADATA
new file mode 100644
index 00000000000..fd2070aede7
--- /dev/null
+++ b/chromium/components/gcm_driver/DIR_METADATA
@@ -0,0 +1,4 @@
+monorail {
+ component: "Services>CloudMessaging"
+}
+team_email: "platform-capabilities@chromium.org"
diff --git a/chromium/components/gcm_driver/OWNERS b/chromium/components/gcm_driver/OWNERS
new file mode 100644
index 00000000000..4dfe4876e05
--- /dev/null
+++ b/chromium/components/gcm_driver/OWNERS
@@ -0,0 +1,2 @@
+fgorski@chromium.org
+peter@chromium.org
diff --git a/chromium/components/gcm_driver/account_tracker.cc b/chromium/components/gcm_driver/account_tracker.cc
new file mode 100644
index 00000000000..20b821ee9e8
--- /dev/null
+++ b/chromium/components/gcm_driver/account_tracker.cc
@@ -0,0 +1,147 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/account_tracker.h"
+
+#include "base/containers/contains.h"
+#include "base/logging.h"
+#include "base/trace_event/trace_event.h"
+#include "components/signin/public/identity_manager/access_token_info.h"
+
+namespace gcm {
+
+AccountTracker::AccountTracker(signin::IdentityManager* identity_manager)
+ : identity_manager_(identity_manager), shutdown_called_(false) {
+ identity_manager_->AddObserver(this);
+}
+
+AccountTracker::~AccountTracker() {
+ DCHECK(shutdown_called_);
+}
+
+void AccountTracker::Shutdown() {
+ shutdown_called_ = true;
+ identity_manager_->RemoveObserver(this);
+}
+
+void AccountTracker::AddObserver(Observer* observer) {
+ observer_list_.AddObserver(observer);
+}
+
+void AccountTracker::RemoveObserver(Observer* observer) {
+ observer_list_.RemoveObserver(observer);
+}
+
+std::vector<CoreAccountInfo> AccountTracker::GetAccounts() const {
+ const CoreAccountId active_account_id =
+ identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSync);
+ std::vector<CoreAccountInfo> accounts;
+
+ for (auto it = accounts_.begin(); it != accounts_.end(); ++it) {
+ const AccountState& state = it->second;
+ DCHECK(!state.account.account_id.empty());
+ DCHECK(!state.account.gaia.empty());
+ DCHECK(!state.account.email.empty());
+ bool is_visible = state.is_signed_in;
+
+ if (it->first == active_account_id) {
+ if (is_visible)
+ accounts.insert(accounts.begin(), state.account);
+ else
+ return std::vector<CoreAccountInfo>();
+ } else if (is_visible) {
+ accounts.push_back(state.account);
+ }
+ }
+ return accounts;
+}
+
+void AccountTracker::OnRefreshTokenUpdatedForAccount(
+ const CoreAccountInfo& account_info) {
+ TRACE_EVENT1("identity", "AccountTracker::OnRefreshTokenUpdatedForAccount",
+ "account_id", account_info.account_id.ToString());
+
+ // Ignore refresh tokens if there is no active account ID at all.
+ if (!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync))
+ return;
+
+ DVLOG(1) << "AVAILABLE " << account_info.account_id;
+ StartTrackingAccount(account_info);
+ UpdateSignInState(account_info.account_id, /*is_signed_in=*/true);
+}
+
+void AccountTracker::OnRefreshTokenRemovedForAccount(
+ const CoreAccountId& account_id) {
+ TRACE_EVENT1("identity", "AccountTracker::OnRefreshTokenRemovedForAccount",
+ "account_id", account_id.ToString());
+
+ DVLOG(1) << "REVOKED " << account_id;
+ UpdateSignInState(account_id, /*is_signed_in=*/false);
+}
+
+void AccountTracker::OnPrimaryAccountChanged(
+ const signin::PrimaryAccountChangeEvent& event) {
+ switch (event.GetEventTypeFor(signin::ConsentLevel::kSync)) {
+ case signin::PrimaryAccountChangeEvent::Type::kSet: {
+ TRACE_EVENT0("identity", "AccountTracker::OnPrimaryAccountSet");
+ std::vector<CoreAccountInfo> accounts =
+ identity_manager_->GetAccountsWithRefreshTokens();
+ DVLOG(1) << "LOGIN " << accounts.size() << " accounts available.";
+ for (const CoreAccountInfo& account_info : accounts) {
+ StartTrackingAccount(account_info);
+ UpdateSignInState(account_info.account_id, /*is_signed_in=*/true);
+ }
+ break;
+ }
+ case signin::PrimaryAccountChangeEvent::Type::kCleared: {
+ TRACE_EVENT0("identity", "AccountTracker::OnPrimaryAccountCleared");
+ DVLOG(1) << "LOGOUT";
+ StopTrackingAllAccounts();
+ break;
+ }
+ case signin::PrimaryAccountChangeEvent::Type::kNone:
+ break;
+ }
+}
+
+void AccountTracker::UpdateSignInState(const CoreAccountId& account_id,
+ bool is_signed_in) {
+ if (!is_signed_in && !base::Contains(accounts_, account_id))
+ return;
+
+ DCHECK(base::Contains(accounts_, account_id));
+ AccountState& account = accounts_[account_id];
+ if (account.is_signed_in == is_signed_in)
+ return;
+
+ account.is_signed_in = is_signed_in;
+ for (auto& observer : observer_list_)
+ observer.OnAccountSignInChanged(account.account, account.is_signed_in);
+}
+
+void AccountTracker::StartTrackingAccount(const CoreAccountInfo& account) {
+ if (base::Contains(accounts_, account.account_id))
+ return;
+
+ DVLOG(1) << "StartTracking " << account.account_id;
+ AccountState account_state;
+ account_state.account = account;
+ account_state.is_signed_in = false;
+ accounts_.insert(std::make_pair(account.account_id, account_state));
+}
+
+void AccountTracker::StopTrackingAccount(const CoreAccountId account_id) {
+ DVLOG(1) << "StopTracking " << account_id;
+ if (base::Contains(accounts_, account_id)) {
+ UpdateSignInState(account_id, /*is_signed_in=*/false);
+ accounts_.erase(account_id);
+ }
+}
+
+void AccountTracker::StopTrackingAllAccounts() {
+ while (!accounts_.empty())
+ StopTrackingAccount(accounts_.begin()->first);
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/account_tracker.h b/chromium/components/gcm_driver/account_tracker.h
new file mode 100644
index 00000000000..61c5def0ddd
--- /dev/null
+++ b/chromium/components/gcm_driver/account_tracker.h
@@ -0,0 +1,78 @@
+// Copyright 2014 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 COMPONENTS_GCM_DRIVER_ACCOUNT_TRACKER_H_
+#define COMPONENTS_GCM_DRIVER_ACCOUNT_TRACKER_H_
+
+#include <map>
+#include <memory>
+#include <vector>
+
+#include "base/memory/raw_ptr.h"
+#include "base/observer_list.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "google_apis/gaia/core_account_id.h"
+
+namespace gcm {
+
+// The AccountTracker keeps track of what accounts exist on the
+// profile and the state of their credentials.
+class AccountTracker : public signin::IdentityManager::Observer {
+ public:
+ explicit AccountTracker(signin::IdentityManager* identity_manager);
+ ~AccountTracker() override;
+
+ class Observer {
+ public:
+ virtual void OnAccountSignInChanged(const CoreAccountInfo& account,
+ bool is_signed_in) = 0;
+ };
+
+ void Shutdown();
+
+ void AddObserver(Observer* observer);
+ void RemoveObserver(Observer* observer);
+
+ // Returns the list of accounts that are signed in, and for which gaia account
+ // have been fetched. The primary account for the profile will be first
+ // in the vector. Additional accounts will be in order of their gaia account.
+ std::vector<CoreAccountInfo> GetAccounts() const;
+
+ private:
+ struct AccountState {
+ CoreAccountInfo account;
+ bool is_signed_in;
+ };
+
+ // signin::IdentityManager::Observer implementation.
+ void OnPrimaryAccountChanged(
+ const signin::PrimaryAccountChangeEvent& event) override;
+ void OnRefreshTokenUpdatedForAccount(
+ const CoreAccountInfo& account_info) override;
+ void OnRefreshTokenRemovedForAccount(
+ const CoreAccountId& account_id) override;
+
+ // Add |account_info| to the lists of accounts tracked by this AccountTracker.
+ void StartTrackingAccount(const CoreAccountInfo& account_info);
+
+ // Stops tracking |account_id|. Notifies all observers if the account was
+ // previously signed in.
+ void StopTrackingAccount(const CoreAccountId account_id);
+
+ // Stops tracking all accounts.
+ void StopTrackingAllAccounts();
+
+ // Updates the is_signed_in corresponding to the given account. Notifies all
+ // observers of the signed in state changes.
+ void UpdateSignInState(const CoreAccountId& account_id, bool is_signed_in);
+
+ raw_ptr<signin::IdentityManager> identity_manager_;
+ std::map<CoreAccountId, AccountState> accounts_;
+ base::ObserverList<Observer>::Unchecked observer_list_;
+ bool shutdown_called_;
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_ACCOUNT_TRACKER_H_
diff --git a/chromium/components/gcm_driver/account_tracker_unittest.cc b/chromium/components/gcm_driver/account_tracker_unittest.cc
new file mode 100644
index 00000000000..4901d75672b
--- /dev/null
+++ b/chromium/components/gcm_driver/account_tracker_unittest.cc
@@ -0,0 +1,539 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/account_tracker.h"
+
+#include <algorithm>
+#include <memory>
+#include <vector>
+
+#include "base/strings/stringprintf.h"
+#include "base/test/task_environment.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+const char kPrimaryAccountEmail[] = "primary_account@example.com";
+
+enum TrackingEventType { SIGN_IN, SIGN_OUT };
+
+class TrackingEvent {
+ public:
+ TrackingEvent(TrackingEventType type,
+ const CoreAccountId& account_id,
+ const std::string& gaia_id)
+ : type_(type), account_id_(account_id), gaia_id_(gaia_id) {}
+
+ TrackingEvent(TrackingEventType type, const CoreAccountInfo& account_info)
+ : type_(type),
+ account_id_(account_info.account_id),
+ gaia_id_(account_info.gaia) {}
+
+ bool operator==(const TrackingEvent& event) const {
+ return type_ == event.type_ && account_id_ == event.account_id_ &&
+ gaia_id_ == event.gaia_id_;
+ }
+
+ std::string ToString() const {
+ const char* typestr = "INVALID";
+ switch (type_) {
+ case SIGN_IN:
+ typestr = " IN";
+ break;
+ case SIGN_OUT:
+ typestr = "OUT";
+ break;
+ }
+ return base::StringPrintf("{ type: %s, account_id: %s, gaia: %s }", typestr,
+ account_id_.ToString().c_str(), gaia_id_.c_str());
+ }
+
+ private:
+ friend bool CompareByUser(TrackingEvent a, TrackingEvent b);
+
+ TrackingEventType type_;
+ CoreAccountId account_id_;
+ std::string gaia_id_;
+};
+
+bool CompareByUser(TrackingEvent a, TrackingEvent b) {
+ return a.account_id_ < b.account_id_;
+}
+
+std::string Str(const std::vector<TrackingEvent>& events) {
+ std::string str = "[";
+ bool needs_comma = false;
+ for (auto it = events.begin(); it != events.end(); ++it) {
+ if (needs_comma)
+ str += ",\n ";
+ needs_comma = true;
+ str += it->ToString();
+ }
+ str += "]";
+ return str;
+}
+
+} // namespace
+
+namespace gcm {
+
+class AccountTrackerObserver : public AccountTracker::Observer {
+ public:
+ AccountTrackerObserver() {}
+ virtual ~AccountTrackerObserver() {}
+
+ testing::AssertionResult CheckEvents();
+ testing::AssertionResult CheckEvents(const TrackingEvent& e1);
+ testing::AssertionResult CheckEvents(const TrackingEvent& e1,
+ const TrackingEvent& e2);
+ testing::AssertionResult CheckEvents(const TrackingEvent& e1,
+ const TrackingEvent& e2,
+ const TrackingEvent& e3);
+ testing::AssertionResult CheckEvents(const TrackingEvent& e1,
+ const TrackingEvent& e2,
+ const TrackingEvent& e3,
+ const TrackingEvent& e4);
+ testing::AssertionResult CheckEvents(const TrackingEvent& e1,
+ const TrackingEvent& e2,
+ const TrackingEvent& e3,
+ const TrackingEvent& e4,
+ const TrackingEvent& e5);
+ testing::AssertionResult CheckEvents(const TrackingEvent& e1,
+ const TrackingEvent& e2,
+ const TrackingEvent& e3,
+ const TrackingEvent& e4,
+ const TrackingEvent& e5,
+ const TrackingEvent& e6);
+ void Clear();
+ void SortEventsByUser();
+
+ // AccountTracker::Observer implementation
+ void OnAccountSignInChanged(const CoreAccountInfo& account,
+ bool is_signed_in) override;
+
+ private:
+ testing::AssertionResult CheckEvents(
+ const std::vector<TrackingEvent>& events);
+
+ std::vector<TrackingEvent> events_;
+};
+
+void AccountTrackerObserver::OnAccountSignInChanged(
+ const CoreAccountInfo& account,
+ bool is_signed_in) {
+ events_.push_back(TrackingEvent(is_signed_in ? SIGN_IN : SIGN_OUT,
+ account.account_id, account.gaia));
+}
+
+void AccountTrackerObserver::Clear() {
+ events_.clear();
+}
+
+void AccountTrackerObserver::SortEventsByUser() {
+ std::stable_sort(events_.begin(), events_.end(), CompareByUser);
+}
+
+testing::AssertionResult AccountTrackerObserver::CheckEvents() {
+ std::vector<TrackingEvent> events;
+ return CheckEvents(events);
+}
+
+testing::AssertionResult AccountTrackerObserver::CheckEvents(
+ const TrackingEvent& e1) {
+ std::vector<TrackingEvent> events;
+ events.push_back(e1);
+ return CheckEvents(events);
+}
+
+testing::AssertionResult AccountTrackerObserver::CheckEvents(
+ const TrackingEvent& e1,
+ const TrackingEvent& e2) {
+ std::vector<TrackingEvent> events;
+ events.push_back(e1);
+ events.push_back(e2);
+ return CheckEvents(events);
+}
+
+testing::AssertionResult AccountTrackerObserver::CheckEvents(
+ const TrackingEvent& e1,
+ const TrackingEvent& e2,
+ const TrackingEvent& e3) {
+ std::vector<TrackingEvent> events;
+ events.push_back(e1);
+ events.push_back(e2);
+ events.push_back(e3);
+ return CheckEvents(events);
+}
+
+testing::AssertionResult AccountTrackerObserver::CheckEvents(
+ const TrackingEvent& e1,
+ const TrackingEvent& e2,
+ const TrackingEvent& e3,
+ const TrackingEvent& e4) {
+ std::vector<TrackingEvent> events;
+ events.push_back(e1);
+ events.push_back(e2);
+ events.push_back(e3);
+ events.push_back(e4);
+ return CheckEvents(events);
+}
+
+testing::AssertionResult AccountTrackerObserver::CheckEvents(
+ const TrackingEvent& e1,
+ const TrackingEvent& e2,
+ const TrackingEvent& e3,
+ const TrackingEvent& e4,
+ const TrackingEvent& e5) {
+ std::vector<TrackingEvent> events;
+ events.push_back(e1);
+ events.push_back(e2);
+ events.push_back(e3);
+ events.push_back(e4);
+ events.push_back(e5);
+ return CheckEvents(events);
+}
+
+testing::AssertionResult AccountTrackerObserver::CheckEvents(
+ const TrackingEvent& e1,
+ const TrackingEvent& e2,
+ const TrackingEvent& e3,
+ const TrackingEvent& e4,
+ const TrackingEvent& e5,
+ const TrackingEvent& e6) {
+ std::vector<TrackingEvent> events;
+ events.push_back(e1);
+ events.push_back(e2);
+ events.push_back(e3);
+ events.push_back(e4);
+ events.push_back(e5);
+ events.push_back(e6);
+ return CheckEvents(events);
+}
+
+testing::AssertionResult AccountTrackerObserver::CheckEvents(
+ const std::vector<TrackingEvent>& events) {
+ std::string maybe_newline = (events.size() + events_.size()) > 2 ? "\n" : "";
+ testing::AssertionResult result(
+ (events_ == events)
+ ? testing::AssertionSuccess()
+ : (testing::AssertionFailure()
+ << "Expected " << maybe_newline << Str(events) << ", "
+ << maybe_newline << "Got " << maybe_newline << Str(events_)));
+ events_.clear();
+ return result;
+}
+
+class AccountTrackerTest : public testing::Test {
+ public:
+ AccountTrackerTest() {}
+
+ ~AccountTrackerTest() override {}
+
+ void SetUp() override {
+ account_tracker_ =
+ std::make_unique<AccountTracker>(identity_test_env_.identity_manager());
+ account_tracker_->AddObserver(&observer_);
+ }
+
+ void TearDown() override {
+ account_tracker_->RemoveObserver(&observer_);
+ account_tracker_->Shutdown();
+ }
+
+ AccountTrackerObserver* observer() { return &observer_; }
+
+ AccountTracker* account_tracker() { return account_tracker_.get(); }
+
+ // Helpers to pass fake events to the tracker.
+
+ // Sets the primary account info. Returns the account ID of the
+ // newly-set account.
+ // NOTE: On ChromeOS, the login callback is never fired in production (since
+ // the underlying GoogleSigninSucceeded callback is never sent). Tests that
+ // exercise functionality dependent on that callback firing are not relevant
+ // on ChromeOS and should simply not run on that platform.
+ CoreAccountInfo SetActiveAccount(const std::string& email) {
+ return identity_test_env_.SetPrimaryAccount(email,
+ signin::ConsentLevel::kSync);
+ }
+
+// Helpers that go through a logout flow.
+// NOTE: On ChromeOS, the logout callback is never fired in production (since
+// the underlying GoogleSignedOut callback is never sent). Tests that exercise
+// functionality dependent on that callback firing are not relevant on ChromeOS
+// and should simply not run on that platform.
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+ void NotifyLogoutOfAllAccounts() { identity_test_env_.ClearPrimaryAccount(); }
+#endif
+
+ CoreAccountInfo AddAccountWithToken(const std::string& email) {
+ return identity_test_env_.MakeAccountAvailable(email);
+ }
+
+ void NotifyTokenAvailable(const CoreAccountId& account_id) {
+ identity_test_env_.SetRefreshTokenForAccount(account_id);
+ }
+
+ void NotifyTokenRevoked(const CoreAccountId& account_id) {
+ identity_test_env_.RemoveRefreshTokenForAccount(account_id);
+ }
+
+ CoreAccountInfo SetupPrimaryLogin() {
+ // Initial setup for tests that start with a signed in profile.
+ CoreAccountInfo primary_account = SetActiveAccount(kPrimaryAccountEmail);
+ NotifyTokenAvailable(primary_account.account_id);
+ observer()->Clear();
+
+ return primary_account;
+ }
+
+ private:
+ // net:: stuff needs IO message loop.
+ base::test::SingleThreadTaskEnvironment task_environment_{
+ base::test::SingleThreadTaskEnvironment::MainThreadType::IO};
+ signin::IdentityTestEnvironment identity_test_env_;
+
+ std::unique_ptr<AccountTracker> account_tracker_;
+ AccountTrackerObserver observer_;
+};
+
+// Primary tests just involve the Active account
+
+TEST_F(AccountTrackerTest, PrimaryNoEventsBeforeLogin) {
+ CoreAccountInfo account = AddAccountWithToken("me@dummy.com");
+ NotifyTokenRevoked(account.account_id);
+
+// Logout is not possible on ChromeOS.
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+ NotifyLogoutOfAllAccounts();
+#endif
+
+ EXPECT_TRUE(observer()->CheckEvents());
+}
+
+TEST_F(AccountTrackerTest, PrimaryLoginThenTokenAvailable) {
+ CoreAccountInfo primary_account = SetActiveAccount(kPrimaryAccountEmail);
+ NotifyTokenAvailable(primary_account.account_id);
+ EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, primary_account)));
+}
+
+TEST_F(AccountTrackerTest, PrimaryRevoke) {
+ CoreAccountInfo primary_account = SetActiveAccount(kPrimaryAccountEmail);
+ NotifyTokenAvailable(primary_account.account_id);
+ observer()->Clear();
+
+ NotifyTokenRevoked(primary_account.account_id);
+ EXPECT_TRUE(
+ observer()->CheckEvents(TrackingEvent(SIGN_OUT, primary_account)));
+}
+
+TEST_F(AccountTrackerTest, PrimaryRevokeThenTokenAvailable) {
+ CoreAccountInfo primary_account = SetActiveAccount(kPrimaryAccountEmail);
+ NotifyTokenAvailable(primary_account.account_id);
+ NotifyTokenRevoked(primary_account.account_id);
+ observer()->Clear();
+
+ NotifyTokenAvailable(primary_account.account_id);
+ EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, primary_account)));
+}
+
+// These tests exercise true login/logout, which are not possible on ChromeOS.
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+TEST_F(AccountTrackerTest, PrimaryTokenAvailableThenLogin) {
+ AddAccountWithToken(kPrimaryAccountEmail);
+ EXPECT_TRUE(observer()->CheckEvents());
+
+ CoreAccountInfo primary_account = SetActiveAccount(kPrimaryAccountEmail);
+ EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, primary_account)));
+}
+
+TEST_F(AccountTrackerTest, PrimaryTokenAvailableAndRevokedThenLogin) {
+ CoreAccountInfo primary_account = AddAccountWithToken(kPrimaryAccountEmail);
+ EXPECT_TRUE(observer()->CheckEvents());
+
+ NotifyTokenRevoked(primary_account.account_id);
+ EXPECT_TRUE(observer()->CheckEvents());
+
+ SetActiveAccount(kPrimaryAccountEmail);
+ EXPECT_TRUE(observer()->CheckEvents());
+}
+
+TEST_F(AccountTrackerTest, PrimaryRevokeThenLogin) {
+ CoreAccountInfo primary_account = SetActiveAccount(kPrimaryAccountEmail);
+ NotifyTokenAvailable(primary_account.account_id);
+ NotifyLogoutOfAllAccounts();
+ observer()->Clear();
+
+ SetActiveAccount(kPrimaryAccountEmail);
+ EXPECT_TRUE(observer()->CheckEvents());
+}
+
+TEST_F(AccountTrackerTest, PrimaryLogoutThenRevoke) {
+ CoreAccountInfo primary_account = SetActiveAccount(kPrimaryAccountEmail);
+ NotifyTokenAvailable(primary_account.account_id);
+ observer()->Clear();
+
+ NotifyLogoutOfAllAccounts();
+ EXPECT_TRUE(
+ observer()->CheckEvents(TrackingEvent(SIGN_OUT, primary_account)));
+
+ NotifyTokenRevoked(primary_account.account_id);
+ EXPECT_TRUE(observer()->CheckEvents());
+}
+
+#endif
+
+// Non-primary accounts
+
+TEST_F(AccountTrackerTest, Available) {
+ SetupPrimaryLogin();
+
+ CoreAccountInfo account = AddAccountWithToken("user@example.com");
+ EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, account)));
+}
+
+TEST_F(AccountTrackerTest, AvailableRevokeAvailable) {
+ SetupPrimaryLogin();
+
+ CoreAccountInfo account = AddAccountWithToken("user@example.com");
+ NotifyTokenRevoked(account.account_id);
+ EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, account),
+ TrackingEvent(SIGN_OUT, account)));
+
+ NotifyTokenAvailable(account.account_id);
+ EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, account)));
+}
+
+TEST_F(AccountTrackerTest, AvailableRevokeRevoke) {
+ SetupPrimaryLogin();
+
+ CoreAccountInfo account = AddAccountWithToken("user@example.com");
+ NotifyTokenRevoked(account.account_id);
+ EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, account),
+ TrackingEvent(SIGN_OUT, account)));
+
+ NotifyTokenRevoked(account.account_id);
+ EXPECT_TRUE(observer()->CheckEvents());
+}
+
+TEST_F(AccountTrackerTest, AvailableAvailable) {
+ SetupPrimaryLogin();
+
+ CoreAccountInfo account = AddAccountWithToken("user@example.com");
+ EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, account)));
+
+ NotifyTokenAvailable(account.account_id);
+ EXPECT_TRUE(observer()->CheckEvents());
+}
+
+TEST_F(AccountTrackerTest, TwoAccounts) {
+ SetupPrimaryLogin();
+
+ CoreAccountInfo alpha_account = AddAccountWithToken("alpha@example.com");
+ EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, alpha_account)));
+
+ CoreAccountInfo beta_account = AddAccountWithToken("beta@example.com");
+ EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_IN, beta_account)));
+
+ NotifyTokenRevoked(alpha_account.account_id);
+ EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_OUT, alpha_account)));
+
+ NotifyTokenRevoked(beta_account.account_id);
+ EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_OUT, beta_account)));
+}
+
+// Primary/non-primary interactions
+
+TEST_F(AccountTrackerTest, MultiNoEventsBeforeLogin) {
+ CoreAccountInfo account1 = AddAccountWithToken("user@example.com");
+ CoreAccountInfo account2 = AddAccountWithToken("user2@example.com");
+ NotifyTokenRevoked(account2.account_id);
+ NotifyTokenRevoked(account2.account_id);
+
+// Logout is not possible on ChromeOS.
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+ NotifyLogoutOfAllAccounts();
+#endif
+
+ EXPECT_TRUE(observer()->CheckEvents());
+}
+
+TEST_F(AccountTrackerTest, MultiRevokePrimaryDoesNotRemoveAllAccounts) {
+ CoreAccountInfo primary_account = SetActiveAccount(kPrimaryAccountEmail);
+ NotifyTokenAvailable(primary_account.account_id);
+ CoreAccountInfo account = AddAccountWithToken("user@example.com");
+ observer()->Clear();
+
+ NotifyTokenRevoked(primary_account.account_id);
+ observer()->SortEventsByUser();
+ EXPECT_TRUE(
+ observer()->CheckEvents(TrackingEvent(SIGN_OUT, primary_account)));
+}
+
+TEST_F(AccountTrackerTest, GetAccountsPrimary) {
+ CoreAccountInfo primary_account = SetupPrimaryLogin();
+
+ std::vector<CoreAccountInfo> account = account_tracker()->GetAccounts();
+ EXPECT_EQ(1ul, account.size());
+ EXPECT_EQ(primary_account.account_id, account[0].account_id);
+ EXPECT_EQ(primary_account.gaia, account[0].gaia);
+ EXPECT_EQ(primary_account.email, account[0].email);
+}
+
+TEST_F(AccountTrackerTest, GetAccountsSignedOut) {
+ std::vector<CoreAccountInfo> accounts = account_tracker()->GetAccounts();
+ EXPECT_EQ(0ul, accounts.size());
+}
+
+TEST_F(AccountTrackerTest, GetMultipleAccounts) {
+ CoreAccountInfo primary_account = SetupPrimaryLogin();
+ CoreAccountInfo alpha_account = AddAccountWithToken("alpha@example.com");
+ CoreAccountInfo beta_account = AddAccountWithToken("beta@example.com");
+
+ std::vector<CoreAccountInfo> account = account_tracker()->GetAccounts();
+ EXPECT_EQ(3ul, account.size());
+ EXPECT_EQ(primary_account.account_id, account[0].account_id);
+ EXPECT_EQ(primary_account.email, account[0].email);
+ EXPECT_EQ(primary_account.gaia, account[0].gaia);
+
+ EXPECT_EQ(alpha_account.account_id, account[1].account_id);
+ EXPECT_EQ(alpha_account.email, account[1].email);
+ EXPECT_EQ(alpha_account.gaia, account[1].gaia);
+
+ EXPECT_EQ(beta_account.account_id, account[2].account_id);
+ EXPECT_EQ(beta_account.email, account[2].email);
+ EXPECT_EQ(beta_account.gaia, account[2].gaia);
+}
+
+TEST_F(AccountTrackerTest, GetAccountsReturnNothingWhenPrimarySignedOut) {
+ CoreAccountInfo primary_account = SetupPrimaryLogin();
+
+ CoreAccountInfo zeta_account = AddAccountWithToken("zeta@example.com");
+ CoreAccountInfo alpha_account = AddAccountWithToken("alpha@example.com");
+
+ NotifyTokenRevoked(primary_account.account_id);
+
+ std::vector<CoreAccountInfo> account = account_tracker()->GetAccounts();
+ EXPECT_EQ(0ul, account.size());
+}
+
+// This test exercises true login/logout, which are not possible on ChromeOS.
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+TEST_F(AccountTrackerTest, MultiLogoutRemovesAllAccounts) {
+ CoreAccountInfo primary_account = SetActiveAccount(kPrimaryAccountEmail);
+ NotifyTokenAvailable(primary_account.account_id);
+ CoreAccountInfo account = AddAccountWithToken("user@example.com");
+ observer()->Clear();
+
+ NotifyLogoutOfAllAccounts();
+ observer()->SortEventsByUser();
+ EXPECT_TRUE(observer()->CheckEvents(TrackingEvent(SIGN_OUT, primary_account),
+ TrackingEvent(SIGN_OUT, account)));
+}
+#endif
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/android/OWNERS b/chromium/components/gcm_driver/android/OWNERS
new file mode 100644
index 00000000000..69b0ace3379
--- /dev/null
+++ b/chromium/components/gcm_driver/android/OWNERS
@@ -0,0 +1,2 @@
+johnme@chromium.org
+mvanouwerkerk@chromium.org
diff --git a/chromium/components/gcm_driver/android/junit/src/org/chromium/components/gcm_driver/GCMMessageTest.java b/chromium/components/gcm_driver/android/junit/src/org/chromium/components/gcm_driver/GCMMessageTest.java
new file mode 100644
index 00000000000..f39d2b5dca7
--- /dev/null
+++ b/chromium/components/gcm_driver/android/junit/src/org/chromium/components/gcm_driver/GCMMessageTest.java
@@ -0,0 +1,267 @@
+// Copyright 2017 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.
+
+package org.chromium.components.gcm_driver;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.os.Bundle;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+
+/**
+ * Unit tests for GCMMessage.
+ */
+@RunWith(BaseRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class GCMMessageTest {
+ private void assertMessageEquals(GCMMessage m1, GCMMessage m2) {
+ assertEquals(m1.getSenderId(), m2.getSenderId());
+ assertEquals(m1.getAppId(), m2.getAppId());
+ assertEquals(m1.getCollapseKey(), m2.getCollapseKey());
+ assertArrayEquals(m1.getDataKeysAndValuesArray(), m2.getDataKeysAndValuesArray());
+ }
+
+ /**
+ * Tests that a message object can be created based on data received from GCM. Note that the raw
+ * data field is tested separately.
+ */
+ @Test
+ public void testCreateMessageFromGCM() {
+ Bundle extras = new Bundle();
+
+ // Compose a simple message that lacks all optional fields.
+ extras.putString("subtype", "MyAppId");
+
+ {
+ GCMMessage message = new GCMMessage("MySenderId", extras);
+
+ assertEquals("MySenderId", message.getSenderId());
+ assertEquals("MyAppId", message.getAppId());
+ assertEquals(null, message.getCollapseKey());
+ assertArrayEquals(new String[] {}, message.getDataKeysAndValuesArray());
+ }
+
+ // Add the optional fields: collapse key, raw binary data, a custom property and an original
+ // priority.
+ extras.putString("collapse_key", "MyCollapseKey");
+ extras.putByteArray("rawData", new byte[] {0x00, 0x15, 0x30, 0x45});
+ extras.putString("property", "value");
+ extras.putString("google.original_priority", "normal");
+
+ {
+ GCMMessage message = new GCMMessage("MySenderId", extras);
+
+ assertEquals("MySenderId", message.getSenderId());
+ assertEquals("MyAppId", message.getAppId());
+ assertEquals("MyCollapseKey", message.getCollapseKey());
+ assertArrayEquals(
+ new String[] {"property", "value"}, message.getDataKeysAndValuesArray());
+ assertEquals(GCMMessage.Priority.NORMAL, message.getOriginalPriority());
+ }
+ }
+
+ /**
+ * Tests that the bundle containing extras from GCM will be filtered for values that we either
+ * pass through by other means, or that we know to be irrelevant to regular GCM messages.
+ */
+ @Test
+ public void testFiltersExtraBundle() {
+ Bundle extras = new Bundle();
+
+ // These should be filtered by full key.
+ extras.putString("collapse_key", "collapseKey");
+ extras.putString("rawData", "rawData");
+ extras.putString("from", "from");
+ extras.putString("subtype", "subtype");
+
+ // These should be filtered by prefix matching.
+ extras.putString("com.google.ipc.invalidation.gcmmplex.foo", "bar");
+ extras.putString("com.google.ipc.invalidation.gcmmplex.bar", "baz");
+
+ // These should be filtered because they're not strings.
+ extras.putBoolean("myBoolean", true);
+ extras.putInt("myInteger", 42);
+
+ // These should not be filtered.
+ extras.putString("collapse_key2", "secondCollapseKey");
+ extras.putString("myString", "foobar");
+
+ GCMMessage message = new GCMMessage("senderId", extras);
+
+ assertArrayEquals(new String[] {"collapse_key2", "secondCollapseKey", "myString", "foobar"},
+ message.getDataKeysAndValuesArray());
+ }
+
+ /**
+ * Tests that a GCMMessage object can be serialized to and deserialized from a persistable
+ * bundle. Note that the raw data field is tested separately. Only run on Android L and beyond
+ * because it depends on PersistableBundle.
+ */
+ @Test
+ public void testSerializationToPersistableBundle() {
+ Bundle extras = new Bundle();
+
+ // Compose a simple message that lacks all optional fields.
+ extras.putString("subtype", "MyAppId");
+
+ {
+ GCMMessage message = new GCMMessage("MySenderId", extras);
+ GCMMessage copiedMessage = GCMMessage.createFromBundle(message.toBundle());
+ assertMessageEquals(message, copiedMessage);
+ }
+
+ // Add the optional fields: collapse key, raw binary data and a custom property.
+ extras.putString("collapse_key", "MyCollapseKey");
+ extras.putString("property", "value");
+ extras.putString("google.original_priority", "normal");
+
+ {
+ GCMMessage message = new GCMMessage("MySenderId", extras);
+ GCMMessage copiedMessage = GCMMessage.createFromBundle(message.toBundle());
+ assertMessageEquals(message, copiedMessage);
+ }
+ }
+
+ /**
+ * Tests that the raw data field can be serialized and deserialized as expected. It should be
+ * NULL when undefined, an empty byte array when defined but empty, and a regular, filled
+ * byte array when data has been provided. Only run on Android L and beyond because it depends
+ * on PersistableBundle.
+ */
+ @Test
+ public void testRawDataSerializationBehaviour() {
+ Bundle extras = new Bundle();
+ extras.putString("subtype", "MyAppId");
+
+ // Case 1: No raw data supplied. Should be NULL.
+ {
+ GCMMessage message = new GCMMessage("MySenderId", extras);
+ GCMMessage copiedMessage = GCMMessage.createFromBundle(message.toBundle());
+
+ assertArrayEquals(null, message.getRawData());
+ assertArrayEquals(null, copiedMessage.getRawData());
+ }
+
+ extras.putByteArray("rawData", new byte[] {});
+
+ // Case 2: Empty byte array of raw data supplied. Should be just that.
+ {
+ GCMMessage message = new GCMMessage("MySenderId", extras);
+ GCMMessage copiedMessage = GCMMessage.createFromBundle(message.toBundle());
+
+ assertArrayEquals(new byte[] {}, message.getRawData());
+ assertArrayEquals(new byte[] {}, copiedMessage.getRawData());
+ }
+
+ extras.putByteArray("rawData", new byte[] {0x00, 0x15, 0x30, 0x45});
+
+ // Case 3: Byte array with data supplied.
+ {
+ GCMMessage message = new GCMMessage("MySenderId", extras);
+ GCMMessage copiedMessage = GCMMessage.createFromBundle(message.toBundle());
+
+ assertArrayEquals(new byte[] {0x00, 0x15, 0x30, 0x45}, message.getRawData());
+ assertArrayEquals(new byte[] {0x00, 0x15, 0x30, 0x45}, copiedMessage.getRawData());
+ }
+ }
+
+ /**
+ * Tests that a GCMMessage object can be serialized to and deserialized from
+ * a JSONObject. Note that the raw data field is tested separately.
+ */
+ @Test
+ public void testSerializationToJSON() throws JSONException {
+ Bundle extras = new Bundle();
+
+ // Compose a simple message that lacks all optional fields.
+ extras.putString("subtype", "MyAppId");
+
+ {
+ GCMMessage message = new GCMMessage("MySenderId", extras);
+ JSONObject messageJSON = message.toJSON();
+
+ // Version must be written to JSON.
+ assertEquals(messageJSON.get("version"), GCMMessage.VERSION);
+ GCMMessage copiedMessage = GCMMessage.createFromJSON(messageJSON);
+
+ assertMessageEquals(message, copiedMessage);
+ }
+
+ // Add the optional fields: collapse key, raw binary data and a custom property.
+ extras.putString("collapse_key", "MyCollapseKey");
+ extras.putString("property", "value");
+ extras.putString("google.original_priority", "normal");
+
+ {
+ GCMMessage message = new GCMMessage("MySenderId", extras);
+ GCMMessage copiedMessage = GCMMessage.createFromJSON(message.toJSON());
+
+ assertMessageEquals(message, copiedMessage);
+ }
+ }
+
+ /**
+ * Tests that the raw data field can be serialized and deserialized as expected from JSONObject.
+ * It should be NULL when undefined, an empty byte array when defined but empty, and a regular,
+ * filled byte array when data has been provided.
+ */
+ @Test
+ public void testRawDataSerializationToJSON() {
+ Bundle extras = new Bundle();
+ extras.putString("subtype", "MyAppId");
+
+ // Case 1: No raw data supplied. Should be NULL.
+ {
+ GCMMessage message = new GCMMessage("MySenderId", extras);
+ GCMMessage copiedMessage = GCMMessage.createFromJSON(message.toJSON());
+
+ assertArrayEquals(null, message.getRawData());
+ assertArrayEquals(null, copiedMessage.getRawData());
+ }
+
+ extras.putByteArray("rawData", new byte[] {});
+
+ // Case 2: Empty byte array of raw data supplied. Should be just that.
+ {
+ GCMMessage message = new GCMMessage("MySenderId", extras);
+ GCMMessage copiedMessage = GCMMessage.createFromJSON(message.toJSON());
+
+ assertArrayEquals(new byte[] {}, message.getRawData());
+ assertArrayEquals(new byte[] {}, copiedMessage.getRawData());
+ }
+ final byte[] rawData = {0x00, 0x15, 0x30, 0x45};
+ extras.putByteArray("rawData", rawData);
+
+ // Case 3: Byte array with data supplied.
+ {
+ GCMMessage message = new GCMMessage("MySenderId", extras);
+ GCMMessage copiedMessage = GCMMessage.createFromJSON(message.toJSON());
+
+ assertArrayEquals(rawData, message.getRawData());
+ assertArrayEquals(rawData, copiedMessage.getRawData());
+ }
+ }
+
+ /**
+ * Tests that getOriginalPriority returns Priority.NONE if it was not set in the bundle.
+ */
+ @Test
+ public void testNullOriginalPriority() {
+ Bundle extras = new Bundle();
+
+ // Compose a simple message that lacks all optional fields.
+ extras.putString("subtype", "MyAppId");
+ GCMMessage message = new GCMMessage("MySenderId", extras);
+
+ assertEquals(GCMMessage.Priority.NONE, message.getOriginalPriority());
+ }
+}
diff --git a/chromium/components/gcm_driver/android/junit/src/org/chromium/components/gcm_driver/LazySubscriptionsManagerTest.java b/chromium/components/gcm_driver/android/junit/src/org/chromium/components/gcm_driver/LazySubscriptionsManagerTest.java
new file mode 100644
index 00000000000..fc77ce0b22f
--- /dev/null
+++ b/chromium/components/gcm_driver/android/junit/src/org/chromium/components/gcm_driver/LazySubscriptionsManagerTest.java
@@ -0,0 +1,267 @@
+// Copyright 2018 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.
+
+package org.chromium.components.gcm_driver;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.SharedPreferences;
+import android.os.Bundle;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+import org.chromium.base.ContextUtils;
+import org.chromium.base.test.BaseRobolectricTestRunner;
+
+import java.util.Set;
+
+/**
+ * Unit tests for LazySubscriptionsManager.
+ */
+@RunWith(BaseRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class LazySubscriptionsManagerTest {
+ /**
+ * Tests the persistence of the "hasPersistedMessages" flag.
+ */
+ @Test
+ public void testHasPersistedMessages() {
+ final String subscriptionId = "subscription_id";
+ // By default there is no persisted messages.
+ assertTrue(LazySubscriptionsManager.getSubscriptionIdsWithPersistedMessages(subscriptionId)
+ .isEmpty());
+
+ LazySubscriptionsManager.storeHasPersistedMessagesForSubscription(subscriptionId, true);
+ assertEquals(1,
+ LazySubscriptionsManager.getSubscriptionIdsWithPersistedMessages(subscriptionId)
+ .size());
+
+ LazySubscriptionsManager.storeHasPersistedMessagesForSubscription(subscriptionId, false);
+ assertTrue(LazySubscriptionsManager.getSubscriptionIdsWithPersistedMessages(subscriptionId)
+ .isEmpty());
+ }
+
+ /**
+ * Tests the migration path from one boolean pref to a set subscription ids for persisted
+ * messages.
+ */
+ @Test
+ public void testMigrateHasPersistedMessagesPref() {
+ final String subscriptionId1 = "subscription_id1";
+ final String subscriptionId2 = "subscription_id2";
+ LazySubscriptionsManager.storeLazinessInformation(subscriptionId1, true);
+ LazySubscriptionsManager.storeLazinessInformation(subscriptionId2, true);
+
+ SharedPreferences sharedPrefs = ContextUtils.getAppSharedPreferences();
+ sharedPrefs.edit()
+ .putBoolean(LazySubscriptionsManager.LEGACY_HAS_PERSISTED_MESSAGES_KEY, false)
+ .apply();
+ LazySubscriptionsManager.migrateHasPersistedMessagesPref();
+
+ assertTrue(LazySubscriptionsManager.getSubscriptionIdsWithPersistedMessages(subscriptionId1)
+ .isEmpty());
+ assertTrue(LazySubscriptionsManager.getSubscriptionIdsWithPersistedMessages(subscriptionId2)
+ .isEmpty());
+
+ sharedPrefs.edit()
+ .putBoolean(LazySubscriptionsManager.LEGACY_HAS_PERSISTED_MESSAGES_KEY, true)
+ .apply();
+ LazySubscriptionsManager.migrateHasPersistedMessagesPref();
+
+ assertEquals(1,
+ LazySubscriptionsManager.getSubscriptionIdsWithPersistedMessages(subscriptionId1)
+ .size());
+ assertEquals(1,
+ LazySubscriptionsManager.getSubscriptionIdsWithPersistedMessages(subscriptionId2)
+ .size());
+ }
+
+ /**
+ * Tests that lazy subscriptions are stored.
+ */
+ @Test
+ public void testMarkSubscriptionAsLazy() {
+ final String subscriptionId = "subscription_id";
+ LazySubscriptionsManager.storeLazinessInformation(subscriptionId, true);
+ assertTrue(LazySubscriptionsManager.isSubscriptionLazy(subscriptionId));
+ }
+
+ /**
+ * Tests that unlazy subscriptions are stored.
+ */
+ @Test
+ public void testMarkSubscriptionAsNotLazy() {
+ final String subscriptionId = "subscription_id";
+ LazySubscriptionsManager.storeLazinessInformation(subscriptionId, false);
+ assertFalse(LazySubscriptionsManager.isSubscriptionLazy(subscriptionId));
+ }
+
+ /**
+ * Tests subscriptions are not lazy be default.
+ */
+ @Test
+ public void testDefaultSubscriptionNotLazy() {
+ final String subscriptionId = "subscription_id";
+ assertFalse(LazySubscriptionsManager.isSubscriptionLazy(subscriptionId));
+ }
+
+ /**
+ * Tests that switching from lazy to unlazy should leave no queued messages behind.
+ */
+ @Test
+ public void testSwitchingFromLazyToUnlazy() {
+ final String subscriptionId = "subscription_id";
+ LazySubscriptionsManager.storeLazinessInformation(subscriptionId, true);
+
+ Bundle extras = new Bundle();
+ extras.putString("subtype", "MyAppId");
+ extras.putString("collapse_key", "CollapseKey");
+ GCMMessage message = new GCMMessage("MySenderId", extras);
+
+ LazySubscriptionsManager.persistMessage(subscriptionId, message);
+ assertEquals(1, LazySubscriptionsManager.readMessages(subscriptionId).length);
+
+ LazySubscriptionsManager.storeLazinessInformation(subscriptionId, false);
+ assertEquals(0, LazySubscriptionsManager.readMessages(subscriptionId).length);
+ }
+
+ /**
+ * Tests that switching from lazy to unlazy and back to lazy.
+ */
+ @Test
+ public void testSwitchingFromLazyToUnlazyAndBackToLazy() {
+ final String subscriptionId = "subscription_id";
+ LazySubscriptionsManager.storeLazinessInformation(subscriptionId, true);
+ assertTrue(LazySubscriptionsManager.isSubscriptionLazy(subscriptionId));
+ LazySubscriptionsManager.storeLazinessInformation(subscriptionId, false);
+ assertFalse(LazySubscriptionsManager.isSubscriptionLazy(subscriptionId));
+ LazySubscriptionsManager.storeLazinessInformation(subscriptionId, true);
+ assertTrue(LazySubscriptionsManager.isSubscriptionLazy(subscriptionId));
+ }
+
+ @Test
+ public void testGetLazySubscriptionIds() {
+ final String subscriptionId1 = "subscription_id1";
+ final String subscriptionId2 = "subscription_id2";
+ final String subscriptionId3 = "subscription_id3";
+ LazySubscriptionsManager.storeLazinessInformation(subscriptionId1, true);
+ LazySubscriptionsManager.storeLazinessInformation(subscriptionId2, true);
+ LazySubscriptionsManager.storeLazinessInformation(subscriptionId3, true);
+ Set<String> lazySubscriptionIds = LazySubscriptionsManager.getLazySubscriptionIds();
+ assertEquals(3, lazySubscriptionIds.size());
+ assertTrue(lazySubscriptionIds.contains(subscriptionId1));
+ assertTrue(lazySubscriptionIds.contains(subscriptionId2));
+ assertTrue(lazySubscriptionIds.contains(subscriptionId3));
+ }
+
+ /**
+ * Tests that GCM messages are persisted and read.
+ */
+ @Test
+ public void testReadingPersistedMessage() {
+ final String subscriptionId = "subscriptionId";
+ final String anotherSubscriptionId = "AnotherSubscriptionId";
+
+ Bundle extras = new Bundle();
+ extras.putString("subtype", "MyAppId");
+ extras.putString("collapse_key", "CollapseKey");
+ GCMMessage message = new GCMMessage("MySenderId", extras);
+ LazySubscriptionsManager.persistMessage(subscriptionId, message);
+
+ GCMMessage messages[] = LazySubscriptionsManager.readMessages(subscriptionId);
+ assertEquals(1, messages.length);
+ assertEquals(message.getSenderId(), messages[0].getSenderId());
+
+ messages = LazySubscriptionsManager.readMessages(anotherSubscriptionId);
+ assertEquals(0, messages.length);
+ }
+
+ /**
+ * Tests that only MESSAGES_QUEUE_SIZE messages are kept.
+ */
+ @Test
+ public void testPersistingMessageCount() {
+ // This tests persists MESSAGES_QUEUE_SIZE+extraMessagesCount messages
+ // and checks if only the most recent |MESSAGES_QUEUE_SIZE| are read.
+ // |collapse_key| is used to distinguish between messages for
+ // simplicity.
+ final String subscriptionId = "subscriptionId";
+ final String collapseKeyPrefix = "subscriptionId";
+ final int extraMessagesCount = 5;
+
+ // Persist |MESSAGES_QUEUE_SIZE| + |extraMessagesCount| messages.
+ for (int i = 0; i < LazySubscriptionsManager.MESSAGES_QUEUE_SIZE + extraMessagesCount;
+ i++) {
+ Bundle extras = new Bundle();
+ extras.putString("subtype", "MyAppId");
+ extras.putString("collapse_key", collapseKeyPrefix + i);
+ GCMMessage message = new GCMMessage("MySenderId", extras);
+ LazySubscriptionsManager.persistMessage(subscriptionId, message);
+ }
+ // Check that only the most recent |MESSAGES_QUEUE_SIZE| are persisted.
+ GCMMessage messages[] = LazySubscriptionsManager.readMessages(subscriptionId);
+ assertEquals(LazySubscriptionsManager.MESSAGES_QUEUE_SIZE, messages.length);
+ for (int i = 0; i < LazySubscriptionsManager.MESSAGES_QUEUE_SIZE; i++) {
+ assertEquals(
+ collapseKeyPrefix + (i + extraMessagesCount), messages[i].getCollapseKey());
+ }
+ }
+
+ /**
+ * Tests that messages with the same collapse key override each other.
+ */
+ @Test
+ public void testCollapseKeyCollision() {
+ final String subscriptionId = "subscriptionId";
+ final String collapseKey = "collapseKey";
+ final byte[] rawData1 = {0x00, 0x15, 0x30, 0x01};
+ final byte[] rawData2 = {0x00, 0x15, 0x30, 0x02};
+
+ Bundle extras = new Bundle();
+ extras.putString("subtype", "MyAppId");
+ extras.putString("collapse_key", collapseKey);
+ extras.putByteArray("rawData", rawData1);
+
+ // Persist a message and make sure it's persisted.
+ GCMMessage message1 = new GCMMessage("MySenderId", extras);
+ LazySubscriptionsManager.persistMessage(subscriptionId, message1);
+
+ GCMMessage messages[] = LazySubscriptionsManager.readMessages(subscriptionId);
+ assertEquals(1, messages.length);
+ assertArrayEquals(rawData1, messages[0].getRawData());
+
+ // Persist another message with the same collapse key and another raw data.
+ extras.putByteArray("rawData", rawData2);
+ GCMMessage message2 = new GCMMessage("MySenderId", extras);
+ LazySubscriptionsManager.persistMessage(subscriptionId, message2);
+
+ messages = LazySubscriptionsManager.readMessages(subscriptionId);
+ assertEquals(1, messages.length);
+ assertArrayEquals(rawData2, messages[0].getRawData());
+ }
+
+ /**
+ * Tests that messages with the same collapse key override each other.
+ */
+ @Test
+ public void testDeletePersistedMessages() {
+ final String subscriptionId = "subscriptionId";
+
+ Bundle extras = new Bundle();
+ extras.putString("subtype", "MyAppId");
+ extras.putString("collapse_key", "collapseKey");
+ extras.putByteArray("rawData", new byte[] {});
+ GCMMessage message = new GCMMessage("MySenderId", extras);
+ LazySubscriptionsManager.persistMessage(subscriptionId, message);
+
+ assertEquals(1, LazySubscriptionsManager.readMessages(subscriptionId).length);
+ LazySubscriptionsManager.deletePersistedMessagesForSubscriptionId(subscriptionId);
+ assertEquals(0, LazySubscriptionsManager.readMessages(subscriptionId).length);
+ }
+}
diff --git a/chromium/components/gcm_driver/common/gcm_driver_export.h b/chromium/components/gcm_driver/common/gcm_driver_export.h
new file mode 100644
index 00000000000..5809ea34435
--- /dev/null
+++ b/chromium/components/gcm_driver/common/gcm_driver_export.h
@@ -0,0 +1,29 @@
+// Copyright 2015 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 COMPONENTS_GCM_DRIVER_COMMON_GCM_DRIVER_EXPORT_H_
+#define COMPONENTS_GCM_DRIVER_COMMON_GCM_DRIVER_EXPORT_H_
+
+#if defined(COMPONENT_BUILD)
+#if defined(WIN32)
+
+#if defined(GCM_DRIVER_IMPLEMENTATION)
+#define GCM_DRIVER_EXPORT __declspec(dllexport)
+#else
+#define GCM_DRIVER_EXPORT __declspec(dllimport)
+#endif // defined(GCM_DRIVER_IMPLEMENTATION)
+
+#else // defined(WIN32)
+#if defined(GCM_DRIVER_IMPLEMENTATION)
+#define GCM_DRIVER_EXPORT __attribute__((visibility("default")))
+#else
+#define GCM_DRIVER_EXPORT
+#endif
+#endif
+
+#else // defined(COMPONENT_BUILD)
+#define GCM_DRIVER_EXPORT
+#endif
+
+#endif // COMPONENTS_GCM_DRIVER_COMMON_GCM_DRIVER_EXPORT_H_
diff --git a/chromium/components/gcm_driver/common/gcm_message.cc b/chromium/components/gcm_driver/common/gcm_message.cc
new file mode 100644
index 00000000000..7937bd8867c
--- /dev/null
+++ b/chromium/components/gcm_driver/common/gcm_message.cc
@@ -0,0 +1,29 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/common/gcm_message.h"
+
+namespace gcm {
+
+// static
+const int OutgoingMessage::kMaximumTTL = 24 * 60 * 60; // 1 day.
+
+OutgoingMessage::OutgoingMessage() = default;
+
+OutgoingMessage::OutgoingMessage(const OutgoingMessage& other) = default;
+
+OutgoingMessage::~OutgoingMessage() = default;
+
+IncomingMessage::IncomingMessage() = default;
+
+IncomingMessage::IncomingMessage(const IncomingMessage& other) = default;
+IncomingMessage::IncomingMessage(IncomingMessage&& other) = default;
+
+IncomingMessage& IncomingMessage::operator=(const IncomingMessage& other) =
+ default;
+IncomingMessage& IncomingMessage::operator=(IncomingMessage&& other) = default;
+
+IncomingMessage::~IncomingMessage() = default;
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/common/gcm_message.h b/chromium/components/gcm_driver/common/gcm_message.h
new file mode 100644
index 00000000000..0226934616b
--- /dev/null
+++ b/chromium/components/gcm_driver/common/gcm_message.h
@@ -0,0 +1,56 @@
+// Copyright 2015 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 COMPONENTS_GCM_DRIVER_COMMON_GCM_MESSAGE_H_
+#define COMPONENTS_GCM_DRIVER_COMMON_GCM_MESSAGE_H_
+
+#include <map>
+#include <string>
+
+#include "components/gcm_driver/common/gcm_driver_export.h"
+
+namespace gcm {
+
+// Message data consisting of key-value pairs.
+using MessageData = std::map<std::string, std::string>;
+
+// Message to be delivered to the other party.
+struct GCM_DRIVER_EXPORT OutgoingMessage {
+ OutgoingMessage();
+ OutgoingMessage(const OutgoingMessage& other);
+ ~OutgoingMessage();
+
+ // Message ID.
+ std::string id;
+ // In seconds.
+ int time_to_live = kMaximumTTL;
+ MessageData data;
+
+ static const int kMaximumTTL;
+};
+
+// Message being received from the other party.
+struct GCM_DRIVER_EXPORT IncomingMessage {
+ IncomingMessage();
+ IncomingMessage(const IncomingMessage& other);
+ IncomingMessage(IncomingMessage&& other);
+ ~IncomingMessage();
+
+ IncomingMessage& operator=(const IncomingMessage& other);
+ IncomingMessage& operator=(IncomingMessage&& other);
+
+ MessageData data;
+ std::string collapse_key;
+ std::string sender_id;
+ std::string message_id;
+ std::string raw_data;
+
+ // Whether the contents of the message have been decrypted, and are
+ // available in |raw_data|.
+ bool decrypted = false;
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_COMMON_GCM_MESSAGE_H_
diff --git a/chromium/components/gcm_driver/crypto/DEPS b/chromium/components/gcm_driver/crypto/DEPS
new file mode 100644
index 00000000000..4060f526a56
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/DEPS
@@ -0,0 +1,7 @@
+include_rules = [
+ "+components/gcm_driver",
+ "+components/leveldb_proto",
+ "+crypto",
+ "+net/http",
+ "+third_party/boringssl/src/include",
+]
diff --git a/chromium/components/gcm_driver/crypto/encryption_header_parsers.cc b/chromium/components/gcm_driver/crypto/encryption_header_parsers.cc
new file mode 100644
index 00000000000..b70dc5276c6
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/encryption_header_parsers.cc
@@ -0,0 +1,156 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/crypto/encryption_header_parsers.h"
+
+#include "base/base64url.h"
+#include "base/strings/string_number_conversions.h"
+
+#include "base/strings/string_util.h"
+
+
+namespace gcm {
+
+namespace {
+
+// The default record size in bytes, as defined in section two of
+// https://tools.ietf.org/html/draft-thomson-http-encryption.
+const uint64_t kDefaultRecordSizeBytes = 4096;
+
+// Decodes the string in |value| using base64url and writes the decoded value to
+// |*salt|. Returns whether the string is not empty and could be decoded.
+bool ValueToDecodedString(base::StringPiece value, std::string* salt) {
+ if (value.empty())
+ return false;
+
+ return base::Base64UrlDecode(
+ value, base::Base64UrlDecodePolicy::IGNORE_PADDING, salt);
+}
+
+// Parses the record size in |value| and writes the value to |*rs|. The value
+// must be a positive decimal integer greater than one that does not start
+// with a plus. Returns whether the record size was valid.
+bool RecordSizeToInt(base::StringPiece value, uint64_t* rs) {
+ if (value.empty())
+ return false;
+
+ // Reject a leading plus, as the fact that the value must be positive is
+ // dictated by the specification.
+ if (value[0] == '+')
+ return false;
+
+ uint64_t candidate_rs;
+ if (!base::StringToUint64(value, &candidate_rs))
+ return false;
+
+ // The record size MUST be greater than one byte.
+ if (candidate_rs <= 1)
+ return false;
+
+ *rs = candidate_rs;
+ return true;
+}
+
+} // namespace
+
+EncryptionHeaderIterator::EncryptionHeaderIterator(
+ std::string::const_iterator header_begin,
+ std::string::const_iterator header_end)
+ : iterator_(header_begin, header_end, ','),
+ rs_(kDefaultRecordSizeBytes) {}
+
+EncryptionHeaderIterator::~EncryptionHeaderIterator() {}
+
+bool EncryptionHeaderIterator::GetNext() {
+ keyid_.clear();
+ salt_.clear();
+ rs_ = kDefaultRecordSizeBytes;
+
+ if (!iterator_.GetNext())
+ return false;
+
+ net::HttpUtil::NameValuePairsIterator name_value_pairs(
+ iterator_.value_begin(), iterator_.value_end(), ';',
+ net::HttpUtil::NameValuePairsIterator::Values::REQUIRED,
+ net::HttpUtil::NameValuePairsIterator::Quotes::NOT_STRICT);
+
+ bool found_keyid = false;
+ bool found_salt = false;
+ bool found_rs = false;
+
+ while (name_value_pairs.GetNext()) {
+ const base::StringPiece name = name_value_pairs.name_piece();
+ const base::StringPiece value = name_value_pairs.value_piece();
+
+ if (base::LowerCaseEqualsASCII(name, "keyid")) {
+ if (found_keyid)
+ return false;
+ keyid_.assign(value.data(), value.size());
+ found_keyid = true;
+ } else if (base::LowerCaseEqualsASCII(name, "salt")) {
+ if (found_salt || !ValueToDecodedString(value, &salt_))
+ return false;
+ found_salt = true;
+ } else if (base::LowerCaseEqualsASCII(name, "rs")) {
+ if (found_rs || !RecordSizeToInt(value, &rs_))
+ return false;
+ found_rs = true;
+ } else {
+ // Silently ignore unknown directives for forward compatibility.
+ }
+ }
+
+ return name_value_pairs.valid();
+}
+
+CryptoKeyHeaderIterator::CryptoKeyHeaderIterator(
+ std::string::const_iterator header_begin,
+ std::string::const_iterator header_end)
+ : iterator_(header_begin, header_end, ',') {}
+
+CryptoKeyHeaderIterator::~CryptoKeyHeaderIterator() {}
+
+bool CryptoKeyHeaderIterator::GetNext() {
+ keyid_.clear();
+ aesgcm128_.clear();
+ dh_.clear();
+
+ if (!iterator_.GetNext())
+ return false;
+
+ net::HttpUtil::NameValuePairsIterator name_value_pairs(
+ iterator_.value_begin(), iterator_.value_end(), ';',
+ net::HttpUtil::NameValuePairsIterator::Values::REQUIRED,
+ net::HttpUtil::NameValuePairsIterator::Quotes::NOT_STRICT);
+
+ bool found_keyid = false;
+ bool found_aesgcm128 = false;
+ bool found_dh = false;
+
+ while (name_value_pairs.GetNext()) {
+ const base::StringPiece name = name_value_pairs.name_piece();
+ const base::StringPiece value = name_value_pairs.value_piece();
+
+ if (base::LowerCaseEqualsASCII(name, "keyid")) {
+ if (found_keyid)
+ return false;
+ keyid_.assign(value.data(), value.size());
+ found_keyid = true;
+ } else if (base::LowerCaseEqualsASCII(name, "aesgcm128")) {
+ if (found_aesgcm128 || !ValueToDecodedString(value, &aesgcm128_))
+ return false;
+ found_aesgcm128 = true;
+ } else if (base::LowerCaseEqualsASCII(name, "dh")) {
+ if (found_dh || !ValueToDecodedString(value, &dh_))
+ return false;
+ found_dh = true;
+ } else {
+ // Silently ignore unknown directives for forward compatibility.
+ }
+ }
+
+ return name_value_pairs.valid();
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/crypto/encryption_header_parsers.h b/chromium/components/gcm_driver/crypto/encryption_header_parsers.h
new file mode 100644
index 00000000000..dc2961a85ff
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/encryption_header_parsers.h
@@ -0,0 +1,92 @@
+// Copyright 2015 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 COMPONENTS_GCM_DRIVER_CRYPTO_ENCRYPTION_HEADER_PARSERS_H_
+#define COMPONENTS_GCM_DRIVER_CRYPTO_ENCRYPTION_HEADER_PARSERS_H_
+
+#include <stdint.h>
+#include <string>
+
+#include "base/strings/string_piece.h"
+#include "net/http/http_util.h"
+
+namespace gcm {
+
+// Iterates over a header that follows the syntax of the Encryption HTTP header
+// per the Encrypted Content-Encoding for HTTP draft. This header follows the
+// #list syntax from the extended ABNF syntax defined in section 1.2 of RFC7230.
+//
+// https://tools.ietf.org/html/draft-thomson-http-encryption#section-3
+// https://tools.ietf.org/html/rfc7230#section-1.2
+class EncryptionHeaderIterator {
+ public:
+ EncryptionHeaderIterator(std::string::const_iterator header_begin,
+ std::string::const_iterator header_end);
+ ~EncryptionHeaderIterator();
+
+ // Advances the iterator to the next header value, if any. Returns true if
+ // there is a next value. Use the keyid(), salt() and rs() methods to access
+ // the key-value pairs included in the header value.
+ bool GetNext();
+
+ const std::string& keyid() const {
+ return keyid_;
+ }
+
+ const std::string& salt() const {
+ return salt_;
+ }
+
+ uint64_t rs() const {
+ return rs_;
+ }
+
+ private:
+ net::HttpUtil::ValuesIterator iterator_;
+
+ std::string keyid_;
+ std::string salt_;
+ uint64_t rs_;
+};
+
+// Iterates over a header that follows the syntax of the Crypto-Key HTTP header
+// per the Encrypted Content-Encoding for HTTP draft. This header follows the
+// #list syntax from the extended ABNF syntax defined in section 1.2 of RFC7230.
+//
+// https://tools.ietf.org/html/draft-thomson-http-encryption#section-4
+// https://tools.ietf.org/html/rfc7230#section-1.2
+class CryptoKeyHeaderIterator {
+ public:
+ CryptoKeyHeaderIterator(std::string::const_iterator header_begin,
+ std::string::const_iterator header_end);
+ ~CryptoKeyHeaderIterator();
+
+ // Advances the iterator to the next header value, if any. Returns true if
+ // there is a next value. Use the keyid(), aesgcm128() and dh() methods to
+ // access the key-value pairs included in the header value.
+ bool GetNext();
+
+ const std::string& keyid() const {
+ return keyid_;
+ }
+
+ const std::string& aesgcm128() const {
+ return aesgcm128_;
+ }
+
+ const std::string& dh() const {
+ return dh_;
+ }
+
+ private:
+ net::HttpUtil::ValuesIterator iterator_;
+
+ std::string keyid_;
+ std::string aesgcm128_;
+ std::string dh_;
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_CRYPTO_ENCRYPTION_HEADER_PARSERS_H_
diff --git a/chromium/components/gcm_driver/crypto/encryption_header_parsers_unittest.cc b/chromium/components/gcm_driver/crypto/encryption_header_parsers_unittest.cc
new file mode 100644
index 00000000000..3fd7026d6a0
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/encryption_header_parsers_unittest.cc
@@ -0,0 +1,374 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/crypto/encryption_header_parsers.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+
+#include "base/cxx17_backports.h"
+#include "base/strings/string_number_conversions.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+
+const uint64_t kDefaultRecordSize = 4096;
+
+TEST(EncryptionHeaderParsersTest, ParseValidEncryptionHeaders) {
+ struct {
+ const char* const header;
+ const char* const parsed_keyid;
+ const char* const parsed_salt;
+ uint64_t parsed_rs;
+ } expected_results[] = {
+ { "keyid=foo;salt=c2l4dGVlbmNvb2xieXRlcw;rs=1024",
+ "foo", "sixteencoolbytes", 1024 },
+ { "keyid=foo; salt=c2l4dGVlbmNvb2xieXRlcw; rs=1024",
+ "foo", "sixteencoolbytes", 1024 },
+ { "KEYID=foo;SALT=c2l4dGVlbmNvb2xieXRlcw;RS=1024",
+ "foo", "sixteencoolbytes", 1024 },
+ { " keyid = foo ; salt = c2l4dGVlbmNvb2xieXRlcw ; rs = 1024 ",
+ "foo", "sixteencoolbytes", 1024 },
+ { "keyid=foo", "foo", "", kDefaultRecordSize },
+ { "keyid=foo;", "foo", "", kDefaultRecordSize },
+ { "keyid=\"foo\"", "foo", "", kDefaultRecordSize },
+ { "salt=c2l4dGVlbmNvb2xieXRlcw",
+ "", "sixteencoolbytes", kDefaultRecordSize },
+ { "rs=2048", "", "", 2048 },
+ { "keyid=foo;someothervalue=1;rs=42", "foo", "", 42 },
+ };
+
+ for (size_t i = 0; i < base::size(expected_results); i++) {
+ SCOPED_TRACE(i);
+
+ std::string header(expected_results[i].header);
+
+ EncryptionHeaderIterator iterator(header.begin(), header.end());
+ ASSERT_TRUE(iterator.GetNext());
+
+ EXPECT_EQ(expected_results[i].parsed_keyid, iterator.keyid());
+ EXPECT_EQ(expected_results[i].parsed_salt, iterator.salt());
+ EXPECT_EQ(expected_results[i].parsed_rs, iterator.rs());
+
+ EXPECT_FALSE(iterator.GetNext());
+ }
+}
+
+TEST(EncryptionHeaderParsersTest, ParseValidMultiValueEncryptionHeaders) {
+ const size_t kNumberOfValues = 2u;
+
+ struct {
+ const char* const header;
+ struct {
+ const char* const keyid;
+ const char* const salt;
+ uint64_t rs;
+ } parsed_values[kNumberOfValues];
+ } expected_results[] = {
+ { "keyid=foo;salt=c2l4dGVlbmNvb2xieXRlcw;rs=1024,keyid=foo;salt=c2l4dGVlbm"
+ "Nvb2xieXRlcw;rs=1024",
+ { { "foo", "sixteencoolbytes", 1024 },
+ { "foo", "sixteencoolbytes", 1024 } } },
+ { "keyid=foo,salt=c2l4dGVlbmNvb2xieXRlcw;rs=1024",
+ { { "foo", "", kDefaultRecordSize },
+ { "", "sixteencoolbytes", 1024 } } },
+ { "keyid=foo,keyid=bar;salt=c2l4dGVlbmNvb2xieXRlcw;rs=1024",
+ { { "foo", "", kDefaultRecordSize },
+ { "bar", "sixteencoolbytes", 1024 } } },
+ { "keyid=\"foo,keyid=bar\",salt=c2l4dGVlbmNvb2xieXRlcw",
+ { { "foo,keyid=bar", "", kDefaultRecordSize },
+ { "", "sixteencoolbytes", kDefaultRecordSize } } },
+ };
+
+ for (size_t i = 0; i < base::size(expected_results); i++) {
+ SCOPED_TRACE(i);
+
+ std::string header(expected_results[i].header);
+
+ EncryptionHeaderIterator iterator(header.begin(), header.end());
+ for (size_t j = 0; j < kNumberOfValues; ++j) {
+ ASSERT_TRUE(iterator.GetNext());
+
+ EXPECT_EQ(expected_results[i].parsed_values[j].keyid, iterator.keyid());
+ EXPECT_EQ(expected_results[i].parsed_values[j].salt, iterator.salt());
+ EXPECT_EQ(expected_results[i].parsed_values[j].rs, iterator.rs());
+ }
+
+ EXPECT_FALSE(iterator.GetNext());
+ }
+}
+
+TEST(EncryptionHeaderParsersTest, ParseInvalidEncryptionHeaders) {
+ const char* const expected_failures[] = {
+ // Values in the name-value pairs are not optional.
+ "keyid",
+ "keyid=",
+ "keyid=foo;keyid",
+ "salt",
+ "salt=",
+ "rs",
+ "rs=",
+
+ // Supplying the same name multiple times in the same value is invalid.
+ "keyid=foo;keyid=bar",
+ "keyid=foo;bar=baz;keyid=qux",
+
+ // The salt must be a URL-safe base64 decodable string.
+ "salt=YmV/2ZXJ-sMDA",
+ "salt=dHdlbHZlY29vbGJ5dGVz=====",
+ "salt=c2l4dGVlbmNvb2xieXRlcw;salt=123$xyz",
+ "salt=123$xyz",
+
+ // The record size must be a positive decimal integer greater than one that
+ // does not start with a plus.
+ "rs=0",
+ "rs=0x13",
+ "rs=1",
+ "rs=-1",
+ "rs=+5",
+ "rs=99999999999999999999999999999999",
+ "rs=foobar",
+ };
+
+ const char* const expected_failures_second_iter[] = {
+ // Valid first field, missing value in the second field.
+ "keyid=foo,novaluekey",
+
+ // Valid first field, undecodable salt in the second field.
+ "salt=c2l4dGVlbmNvb2xieXRlcw,salt=123$xyz",
+
+ // Valid first field, invalid record size in the second field.
+ "rs=2,rs=0",
+ };
+
+ for (size_t i = 0; i < base::size(expected_failures); i++) {
+ SCOPED_TRACE(i);
+
+ std::string header(expected_failures[i]);
+
+ EncryptionHeaderIterator iterator(header.begin(), header.end());
+ EXPECT_FALSE(iterator.GetNext());
+ }
+
+ for (size_t i = 0; i < base::size(expected_failures_second_iter); i++) {
+ SCOPED_TRACE(i);
+
+ std::string header(expected_failures_second_iter[i]);
+
+ EncryptionHeaderIterator iterator(header.begin(), header.end());
+ EXPECT_TRUE(iterator.GetNext());
+ EXPECT_FALSE(iterator.GetNext());
+ }
+}
+
+TEST(EncryptionHeaderParsersTest, ParseValidCryptoKeyHeaders) {
+ struct {
+ const char* const header;
+ const char* const parsed_keyid;
+ const char* const parsed_aesgcm128;
+ const char* const parsed_dh;
+ } expected_results[] = {
+ { "keyid=foo;aesgcm128=c2l4dGVlbmNvb2xieXRlcw;dh=dHdlbHZlY29vbGJ5dGVz",
+ "foo", "sixteencoolbytes", "twelvecoolbytes" },
+ { "keyid=foo; aesgcm128=c2l4dGVlbmNvb2xieXRlcw; dh=dHdlbHZlY29vbGJ5dGVz",
+ "foo", "sixteencoolbytes", "twelvecoolbytes" },
+ { "keyid = foo ; aesgcm128 = c2l4dGVlbmNvb2xieXRlcw ; dh = dHdlbHZlY29vbGJ5"
+ "dGVz ",
+ "foo", "sixteencoolbytes", "twelvecoolbytes" },
+ { "KEYID=foo;AESGCM128=c2l4dGVlbmNvb2xieXRlcw;DH=dHdlbHZlY29vbGJ5dGVz",
+ "foo", "sixteencoolbytes", "twelvecoolbytes" },
+ { "keyid=foo", "foo", "", "" },
+ { "aesgcm128=c2l4dGVlbmNvb2xieXRlcw", "", "sixteencoolbytes", "" },
+ { "aesgcm128=\"c2l4dGVlbmNvb2xieXRlcw\"", "", "sixteencoolbytes", "" },
+ { "dh=dHdlbHZlY29vbGJ5dGVz", "", "", "twelvecoolbytes" },
+ { "keyid=foo;someothervalue=bar;aesgcm128=dHdlbHZlY29vbGJ5dGVz",
+ "foo", "twelvecoolbytes", "" },
+ };
+
+ for (size_t i = 0; i < base::size(expected_results); i++) {
+ SCOPED_TRACE(i);
+
+ std::string header(expected_results[i].header);
+
+ CryptoKeyHeaderIterator iterator(header.begin(), header.end());
+ ASSERT_TRUE(iterator.GetNext());
+
+ EXPECT_EQ(expected_results[i].parsed_keyid, iterator.keyid());
+ EXPECT_EQ(expected_results[i].parsed_aesgcm128, iterator.aesgcm128());
+ EXPECT_EQ(expected_results[i].parsed_dh, iterator.dh());
+
+ EXPECT_FALSE(iterator.GetNext());
+ }
+}
+
+TEST(EncryptionHeaderParsersTest, ParseValidMultiValueCryptoKeyHeaders) {
+ const size_t kNumberOfValues = 2u;
+
+ struct {
+ const char* const header;
+ struct {
+ const char* const keyid;
+ const char* const aesgcm128;
+ const char* const dh;
+ } parsed_values[kNumberOfValues];
+ } expected_results[] = {
+ { "keyid=foo;aesgcm128=c2l4dGVlbmNvb2xieXRlcw;dh=dHdlbHZlY29vbGJ5dGVz,"
+ "keyid=bar;aesgcm128=dHdlbHZlY29vbGJ5dGVz;dh=c2l4dGVlbmNvb2xieXRlcw",
+ { { "foo", "sixteencoolbytes", "twelvecoolbytes" },
+ { "bar", "twelvecoolbytes", "sixteencoolbytes" } } },
+ { "keyid=foo,aesgcm128=c2l4dGVlbmNvb2xieXRlcw",
+ { { "foo", "", "" },
+ { "", "sixteencoolbytes", "" } } },
+ { "keyid=foo,keyid=bar;dh=dHdlbHZlY29vbGJ5dGVz",
+ { { "foo", "", "" },
+ { "bar", "", "twelvecoolbytes" } } },
+ { "keyid=\"foo,keyid=bar\",aesgcm128=c2l4dGVlbmNvb2xieXRlcw",
+ { { "foo,keyid=bar", "", "" },
+ { "", "sixteencoolbytes", "" } } },
+ };
+
+ for (size_t i = 0; i < base::size(expected_results); i++) {
+ SCOPED_TRACE(i);
+
+ std::string header(expected_results[i].header);
+
+ CryptoKeyHeaderIterator iterator(header.begin(), header.end());
+ for (size_t j = 0; j < kNumberOfValues; ++j) {
+ ASSERT_TRUE(iterator.GetNext());
+
+ EXPECT_EQ(expected_results[i].parsed_values[j].keyid, iterator.keyid());
+ EXPECT_EQ(expected_results[i].parsed_values[j].aesgcm128,
+ iterator.aesgcm128());
+ EXPECT_EQ(expected_results[i].parsed_values[j].dh, iterator.dh());
+ }
+
+ EXPECT_FALSE(iterator.GetNext());
+ }
+}
+
+TEST(EncryptionHeaderParsersTest, DISABLED_ParseInvalidCryptoKeyHeaders) {
+ const char* const expected_failures[] = {
+ // Values in the name-value pairs are not optional.
+ "keyid",
+ "keyid=",
+ "keyid=foo;keyid",
+ "aesgcm128",
+ "aesgcm128=",
+ "dh",
+ "dh=",
+
+ // Supplying the same name multiple times in the same value is invalid.
+ "keyid=foo;keyid=bar",
+ "keyid=foo;bar=baz;keyid=qux",
+
+ // The "aesgcm128" parameter must be a URL-safe base64 decodable string.
+ "aesgcm128=123$xyz",
+ "aesgcm128=foobar;aesgcm128=123$xyz",
+
+ // The "dh" parameter must be a URL-safe base64 decodable string.
+ "dh=YmV/2ZXJ-sMDA",
+ "dh=dHdlbHZlY29vbGJ5dGVz=====",
+ "dh=123$xyz",
+ };
+
+ const char* const expected_failures_second_iter[] = {
+ // Valid first field, missing value in the second field.
+ "keyid=foo,novaluekey",
+
+ // Valid first field, undecodable aesgcm128 value in the second field.
+ "dh=dHdlbHZlY29vbGJ5dGVz,aesgcm128=123$xyz",
+ };
+
+ for (size_t i = 0; i < base::size(expected_failures); i++) {
+ SCOPED_TRACE(i);
+
+ std::string header(expected_failures[i]);
+
+ CryptoKeyHeaderIterator iterator(header.begin(), header.end());
+ EXPECT_FALSE(iterator.GetNext());
+ }
+
+ for (size_t i = 0; i < base::size(expected_failures_second_iter); i++) {
+ SCOPED_TRACE(i);
+
+ std::string header(expected_failures_second_iter[i]);
+
+ CryptoKeyHeaderIterator iterator(header.begin(), header.end());
+ EXPECT_TRUE(iterator.GetNext());
+ EXPECT_FALSE(iterator.GetNext());
+ }
+}
+
+TEST(EncryptionHeaderParsersTest, SixValueHeader) {
+ const std::string header("keyid=0,keyid=1,keyid=2,keyid=3,keyid=4,keyid=5");
+
+ EncryptionHeaderIterator encryption_iterator(header.begin(), header.end());
+ CryptoKeyHeaderIterator crypto_key_iterator(header.begin(), header.end());
+
+ for (size_t i = 0; i < 6; ++i) {
+ SCOPED_TRACE(i);
+
+ ASSERT_TRUE(encryption_iterator.GetNext());
+ ASSERT_TRUE(crypto_key_iterator.GetNext());
+ }
+
+ EXPECT_FALSE(encryption_iterator.GetNext());
+ EXPECT_FALSE(crypto_key_iterator.GetNext());
+}
+
+TEST(EncryptionHeaderParsersTest, InvalidHeadersResetOutput) {
+ // Valid first field, invalid record size parameter in the second field.
+ const std::string encryption_header(
+ "keyid=foo;salt=c2l4dGVlbmNvb2xieXRlcw;rs=1024,rs=foobar");
+
+ // Valid first field, undecodable aesgcm128 parameter in the second field.
+ const std::string crypto_key_header(
+ "keyid=foo;aesgcm128=c2l4dGVlbmNvb2xieXRlcw;dh=dHdlbHZlY29vbGJ5dGVz,"
+ "aesgcm128=$$$");
+
+ EncryptionHeaderIterator encryption_iterator(
+ encryption_header.begin(), encryption_header.end());
+
+ ASSERT_EQ(0u, encryption_iterator.keyid().size());
+ ASSERT_EQ(0u, encryption_iterator.salt().size());
+ ASSERT_EQ(4096u, encryption_iterator.rs());
+
+ ASSERT_TRUE(encryption_iterator.GetNext());
+
+ EXPECT_EQ("foo", encryption_iterator.keyid());
+ EXPECT_EQ("sixteencoolbytes", encryption_iterator.salt());
+ EXPECT_EQ(1024u, encryption_iterator.rs());
+
+ ASSERT_FALSE(encryption_iterator.GetNext());
+
+ EXPECT_EQ(0u, encryption_iterator.keyid().size());
+ EXPECT_EQ(0u, encryption_iterator.salt().size());
+ EXPECT_EQ(4096u, encryption_iterator.rs());
+
+ CryptoKeyHeaderIterator crypto_key_iterator(
+ crypto_key_header.begin(), crypto_key_header.end());
+
+ ASSERT_EQ(0u, crypto_key_iterator.keyid().size());
+ ASSERT_EQ(0u, crypto_key_iterator.aesgcm128().size());
+ ASSERT_EQ(0u, crypto_key_iterator.dh().size());
+
+ ASSERT_TRUE(crypto_key_iterator.GetNext());
+
+ EXPECT_EQ("foo", crypto_key_iterator.keyid());
+ EXPECT_EQ("sixteencoolbytes", crypto_key_iterator.aesgcm128());
+ EXPECT_EQ("twelvecoolbytes", crypto_key_iterator.dh());
+
+ ASSERT_FALSE(crypto_key_iterator.GetNext());
+
+ EXPECT_EQ(0u, crypto_key_iterator.keyid().size());
+ EXPECT_EQ(0u, crypto_key_iterator.aesgcm128().size());
+ EXPECT_EQ(0u, crypto_key_iterator.dh().size());
+}
+
+} // namespace
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/crypto/gcm_crypto_test_helpers.cc b/chromium/components/gcm_driver/crypto/gcm_crypto_test_helpers.cc
new file mode 100644
index 00000000000..00c0d16cb61
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/gcm_crypto_test_helpers.cc
@@ -0,0 +1,84 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/crypto/gcm_crypto_test_helpers.h"
+
+#include <stddef.h>
+
+#include <sstream>
+#include <string>
+
+#include "base/base64url.h"
+#include "base/strings/string_util.h"
+#include "components/gcm_driver/common/gcm_message.h"
+#include "components/gcm_driver/crypto/gcm_message_cryptographer.h"
+#include "components/gcm_driver/crypto/p256_key_util.h"
+#include "crypto/ec_private_key.h"
+#include "crypto/random.h"
+
+namespace gcm {
+
+bool CreateEncryptedPayloadForTesting(const base::StringPiece& payload,
+ const base::StringPiece& peer_public_key,
+ const base::StringPiece& auth_secret,
+ IncomingMessage* message) {
+ DCHECK(message);
+
+ // Create an ephemeral key for the sender.
+ std::unique_ptr<crypto::ECPrivateKey> key = crypto::ECPrivateKey::Create();
+ if (!key)
+ return false;
+
+ std::string shared_secret;
+ // Calculate the shared secret between the sender and its peer.
+ if (!ComputeSharedP256Secret(*key, peer_public_key, &shared_secret)) {
+ return false;
+ }
+
+ std::string salt;
+
+ // Generate a cryptographically secure random salt for the message.
+ const size_t salt_size = GCMMessageCryptographer::kSaltSize;
+ crypto::RandBytes(base::WriteInto(&salt, salt_size + 1), salt_size);
+
+ GCMMessageCryptographer cryptographer(
+ GCMMessageCryptographer::Version::DRAFT_03);
+
+ size_t record_size;
+ std::string ciphertext;
+
+ std::string public_key;
+ if (!GetRawPublicKey(*key, &public_key))
+ return false;
+ if (!cryptographer.Encrypt(peer_public_key, public_key, shared_secret,
+ auth_secret, salt, payload, &record_size,
+ &ciphertext)) {
+ return false;
+ }
+
+ std::string encoded_salt, encoded_public_key;
+
+ // Create base64url encoded representations of the salt and local public key.
+ base::Base64UrlEncode(salt, base::Base64UrlEncodePolicy::OMIT_PADDING,
+ &encoded_salt);
+ base::Base64UrlEncode(public_key, base::Base64UrlEncodePolicy::OMIT_PADDING,
+ &encoded_public_key);
+
+ // Write the Encryption header value to |*message|.
+ std::stringstream encryption_header;
+ encryption_header << "salt=" << encoded_salt << ";rs=" << record_size;
+
+ message->data["encryption"] = encryption_header.str();
+
+ // Write the Crypto-Key value to |*message|.
+ std::stringstream crypto_key_header;
+ crypto_key_header << "dh=" << encoded_public_key;
+
+ message->data["crypto-key"] = crypto_key_header.str();
+
+ message->raw_data.swap(ciphertext);
+ return true;
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/crypto/gcm_crypto_test_helpers.h b/chromium/components/gcm_driver/crypto/gcm_crypto_test_helpers.h
new file mode 100644
index 00000000000..8579ccf1f90
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/gcm_crypto_test_helpers.h
@@ -0,0 +1,25 @@
+// Copyright 2015 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 COMPONENTS_GCM_DRIVER_CRYPTO_GCM_CRYPTO_TEST_HELPERS_H_
+#define COMPONENTS_GCM_DRIVER_CRYPTO_GCM_CRYPTO_TEST_HELPERS_H_
+
+#include "base/strings/string_piece.h"
+
+namespace gcm {
+
+struct IncomingMessage;
+
+// Creates an encrypted representation of |payload| using the |peer_public_key|
+// (as an octet string in uncompressed form per SEC1 2.3.3) and the
+// |auth_secret|. Returns whether the payload could be created and has been
+// written to the |*message|.
+bool CreateEncryptedPayloadForTesting(const base::StringPiece& payload,
+ const base::StringPiece& peer_public_key,
+ const base::StringPiece& auth_secret,
+ IncomingMessage* message);
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_CRYPTO_GCM_CRYPTO_TEST_HELPERS_H_
diff --git a/chromium/components/gcm_driver/crypto/gcm_decryption_result.cc b/chromium/components/gcm_driver/crypto/gcm_decryption_result.cc
new file mode 100644
index 00000000000..d5cba73bc37
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/gcm_decryption_result.cc
@@ -0,0 +1,50 @@
+// Copyright 2017 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.
+
+#include "components/gcm_driver/crypto/gcm_decryption_result.h"
+
+#include "base/notreached.h"
+
+namespace gcm {
+
+std::string ToGCMDecryptionResultDetailsString(GCMDecryptionResult result) {
+ switch (result) {
+ case GCMDecryptionResult::UNENCRYPTED:
+ return "Message was not encrypted";
+ case GCMDecryptionResult::DECRYPTED_DRAFT_03:
+ return "Message decrypted (draft 03)";
+ case GCMDecryptionResult::DECRYPTED_DRAFT_08:
+ return "Message decrypted (draft 08)";
+ case GCMDecryptionResult::INVALID_ENCRYPTION_HEADER:
+ return "Invalid format for the Encryption header";
+ case GCMDecryptionResult::INVALID_CRYPTO_KEY_HEADER:
+ return "Invalid format for the Crypto-Key header";
+ case GCMDecryptionResult::NO_KEYS:
+ return "There are no associated keys with the subscription";
+ case GCMDecryptionResult::INVALID_SHARED_SECRET:
+ return "The shared secret cannot be derived from the keying material";
+ case GCMDecryptionResult::INVALID_PAYLOAD:
+ return "AES-GCM decryption failed";
+ case GCMDecryptionResult::INVALID_BINARY_HEADER_PAYLOAD_LENGTH:
+ return "The message payload is smaller than the smallest valid message "
+ "(104 bytes)";
+ case GCMDecryptionResult::INVALID_BINARY_HEADER_RECORD_SIZE:
+ return "The record size indicated in the binary message header is "
+ "smaller than the smallest valid record size (18 bytes)";
+ case GCMDecryptionResult::INVALID_BINARY_HEADER_PUBLIC_KEY_LENGTH:
+ return "The public key included in the binary message header must be a "
+ "valid P-256 ECDH uncompressed point that is 65 bytes in length.";
+ case GCMDecryptionResult::INVALID_BINARY_HEADER_PUBLIC_KEY_FORMAT:
+ return "The public key included in the binary message header must be a "
+ "valid P-256 ECDH uncompressed poin that starts with an 0x04 "
+ "byte.";
+ case GCMDecryptionResult::ENUM_SIZE:
+ break; // deliberate fall-through
+ }
+
+ NOTREACHED();
+ return "(invalid result)";
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/crypto/gcm_decryption_result.h b/chromium/components/gcm_driver/crypto/gcm_decryption_result.h
new file mode 100644
index 00000000000..5f073aa9312
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/gcm_decryption_result.h
@@ -0,0 +1,71 @@
+// Copyright 2017 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 COMPONENTS_GCM_DRIVER_CRYPTO_GCM_DECRYPTION_RESULT_H_
+#define COMPONENTS_GCM_DRIVER_CRYPTO_GCM_DECRYPTION_RESULT_H_
+
+#include <string>
+
+namespace gcm {
+
+// Result of decrypting an incoming message. The values of these reasons must
+// not be changed as they are being recorded using UMA. When adding a value,
+// please update GCMDecryptionResult in //tools/metrics/histograms/enums.xml.
+enum class GCMDecryptionResult {
+ // The message had not been encrypted by the sender.
+ UNENCRYPTED = 0,
+
+ // The message had been encrypted by the sender, and could successfully be
+ // decrypted for the registration it has been received for. The encryption
+ // scheme used for the message was draft-ietf-webpush-encryption-03.
+ DECRYPTED_DRAFT_03 = 1,
+
+ // The contents of the Encryption HTTP header could not be parsed.
+ INVALID_ENCRYPTION_HEADER = 2,
+
+ // The contents of the Crypto-Key HTTP header could not be parsed.
+ INVALID_CRYPTO_KEY_HEADER = 3,
+
+ // No public/private key-pair was associated with the app_id.
+ NO_KEYS = 4,
+
+ // The shared secret cannot be derived from the keying material.
+ INVALID_SHARED_SECRET = 5,
+
+ // The payload could not be decrypted as AES-128-GCM.
+ INVALID_PAYLOAD = 6,
+
+ // Removed in favour of the more detailed reasons below (values 9-13).
+ // INVALID_BINARY_HEADER = 7,
+
+ // The message had been encrypted by the sender, and could successfully be
+ // decrypted for the registration it has been received for. The encryption
+ // scheme used for the message was draft-ietf-webpush-encryption-08.
+ DECRYPTED_DRAFT_08 = 8,
+
+ // The payload's length is smaller than the smallest valid message.
+ INVALID_BINARY_HEADER_PAYLOAD_LENGTH = 9,
+
+ // The payload's record size is smaller than the smallest valid record + 1.
+ INVALID_BINARY_HEADER_RECORD_SIZE = 10,
+
+ // The public key included in the payload does not have the length
+ // corresponding to an uncompressed P-256 ECDH key (65 bytes).
+ INVALID_BINARY_HEADER_PUBLIC_KEY_LENGTH = 11,
+
+ // The public key included in the message does not adhere to the format of
+ // an uncompressed P-256 ECDH key. (I.e. it must start with 0x04.)
+ INVALID_BINARY_HEADER_PUBLIC_KEY_FORMAT = 12,
+
+ // Should be one more than the otherwise highest value in this enumeration.
+ ENUM_SIZE = INVALID_BINARY_HEADER_PUBLIC_KEY_FORMAT + 1
+};
+
+// Converts the GCMDecryptionResult value to a string that can be used to
+// explain the issue on chrome://gcm-internals/.
+std::string ToGCMDecryptionResultDetailsString(GCMDecryptionResult result);
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_CRYPTO_GCM_DECRYPTION_RESULT_H_
diff --git a/chromium/components/gcm_driver/crypto/gcm_encryption_provider.cc b/chromium/components/gcm_driver/crypto/gcm_encryption_provider.cc
new file mode 100644
index 00000000000..d7da79727db
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/gcm_encryption_provider.cc
@@ -0,0 +1,412 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/crypto/gcm_encryption_provider.h"
+
+#include <memory>
+
+#include "base/base64.h"
+#include "base/big_endian.h"
+#include "base/bind.h"
+#include "base/logging.h"
+#include "base/strings/strcat.h"
+#include "components/gcm_driver/common/gcm_message.h"
+#include "components/gcm_driver/crypto/encryption_header_parsers.h"
+#include "components/gcm_driver/crypto/gcm_decryption_result.h"
+#include "components/gcm_driver/crypto/gcm_encryption_result.h"
+#include "components/gcm_driver/crypto/gcm_key_store.h"
+#include "components/gcm_driver/crypto/gcm_message_cryptographer.h"
+#include "components/gcm_driver/crypto/message_payload_parser.h"
+#include "components/gcm_driver/crypto/p256_key_util.h"
+#include "components/gcm_driver/crypto/proto/gcm_encryption_data.pb.h"
+#include "crypto/ec_private_key.h"
+#include "crypto/random.h"
+
+namespace gcm {
+
+namespace {
+
+const char kEncryptionProperty[] = "encryption";
+const char kCryptoKeyProperty[] = "crypto-key";
+const char kInternalRawData[] = "_googRawData";
+
+// Directory in the GCM Store in which the encryption database will be stored.
+const base::FilePath::CharType kEncryptionDirectoryName[] =
+ FILE_PATH_LITERAL("Encryption");
+
+IncomingMessage CreateMessageWithId(const std::string& message_id) {
+ IncomingMessage message;
+ message.message_id = message_id;
+ return message;
+}
+
+} // namespace
+
+GCMEncryptionProvider::GCMEncryptionProvider() {}
+
+GCMEncryptionProvider::~GCMEncryptionProvider() = default;
+
+// static
+const char GCMEncryptionProvider::kContentEncodingProperty[] =
+ "content-encoding";
+
+// static
+const char GCMEncryptionProvider::kContentCodingAes128Gcm[] = "aes128gcm";
+
+void GCMEncryptionProvider::Init(
+ const base::FilePath& store_path,
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner) {
+ DCHECK(!key_store_);
+
+ base::FilePath encryption_store_path = store_path;
+
+ // |store_path| can be empty in tests, which means that the database should
+ // be created in memory rather than on-disk.
+ if (!store_path.empty())
+ encryption_store_path = store_path.Append(kEncryptionDirectoryName);
+
+ key_store_ = std::make_unique<GCMKeyStore>(encryption_store_path,
+ blocking_task_runner);
+}
+
+void GCMEncryptionProvider::GetEncryptionInfo(
+ const std::string& app_id,
+ const std::string& authorized_entity,
+ EncryptionInfoCallback callback) {
+ DCHECK(key_store_);
+ key_store_->GetKeys(
+ app_id, authorized_entity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMEncryptionProvider::DidGetEncryptionInfo,
+ weak_ptr_factory_.GetWeakPtr(), app_id, authorized_entity,
+ std::move(callback)));
+}
+
+void GCMEncryptionProvider::DidGetEncryptionInfo(
+ const std::string& app_id,
+ const std::string& authorized_entity,
+ EncryptionInfoCallback callback,
+ std::unique_ptr<crypto::ECPrivateKey> key,
+ const std::string& auth_secret) {
+ if (!key) {
+ key_store_->CreateKeys(
+ app_id, authorized_entity,
+ base::BindOnce(&GCMEncryptionProvider::DidCreateEncryptionInfo,
+ weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+ return;
+ }
+
+ std::string public_key;
+ const bool success = GetRawPublicKey(*key, &public_key);
+ DCHECK(success);
+ std::move(callback).Run(public_key, auth_secret);
+}
+
+void GCMEncryptionProvider::RemoveEncryptionInfo(
+ const std::string& app_id,
+ const std::string& authorized_entity,
+ base::OnceClosure callback) {
+ DCHECK(key_store_);
+ key_store_->RemoveKeys(app_id, authorized_entity, std::move(callback));
+}
+
+bool GCMEncryptionProvider::IsEncryptedMessage(
+ const IncomingMessage& message) const {
+ // Messages that explicitly specify their content coding to be "aes128gcm"
+ // indicate that they use draft-ietf-webpush-encryption-08.
+ auto content_encoding_iter = message.data.find(kContentEncodingProperty);
+ if (content_encoding_iter != message.data.end() &&
+ content_encoding_iter->second == kContentCodingAes128Gcm) {
+ return true;
+ }
+
+ // The Web Push protocol requires the encryption and crypto-key properties to
+ // be set, and the raw_data field to be populated with the payload.
+ if (message.data.find(kEncryptionProperty) == message.data.end() ||
+ message.data.find(kCryptoKeyProperty) == message.data.end())
+ return false;
+
+ return message.raw_data.size() > 0;
+}
+
+void GCMEncryptionProvider::DecryptMessage(const std::string& app_id,
+ const IncomingMessage& message,
+ DecryptMessageCallback callback) {
+ DCHECK(key_store_);
+ if (!IsEncryptedMessage(message)) {
+ std::move(callback).Run(GCMDecryptionResult::UNENCRYPTED, message);
+ return;
+ }
+
+ std::string salt, public_key, ciphertext;
+ GCMMessageCryptographer::Version version;
+ uint32_t record_size;
+
+ auto content_encoding_iter = message.data.find(kContentEncodingProperty);
+ if (content_encoding_iter != message.data.end() &&
+ content_encoding_iter->second == kContentCodingAes128Gcm) {
+ // The message follows encryption per draft-ietf-webpush-encryption-08. Use
+ // the binary header of the message to derive the values.
+
+ auto parser = std::make_unique<MessagePayloadParser>(message.raw_data);
+ if (!parser->IsValid()) {
+ // Attempt to parse base64 encoded internal raw data.
+ auto raw_data_iter = message.data.find(kInternalRawData);
+ std::string raw_data;
+ if (raw_data_iter == message.data.end() ||
+ !base::Base64Decode(raw_data_iter->second, &raw_data) ||
+ !(parser = std::make_unique<MessagePayloadParser>(raw_data))
+ ->IsValid()) {
+ DLOG(ERROR) << "Unable to parse the message's binary header";
+ std::move(callback).Run(parser->GetFailureReason(),
+ CreateMessageWithId(message.message_id));
+ return;
+ }
+ }
+
+ salt = parser->salt();
+ public_key = parser->public_key();
+ record_size = parser->record_size();
+ ciphertext = parser->ciphertext();
+ version = GCMMessageCryptographer::Version::DRAFT_08;
+ } else {
+ // The message follows encryption per draft-ietf-webpush-encryption-03. Use
+ // the Encryption and Crypto-Key header values to derive the values.
+
+ const auto& encryption_header = message.data.find(kEncryptionProperty);
+ DCHECK(encryption_header != message.data.end());
+
+ const auto& crypto_key_header = message.data.find(kCryptoKeyProperty);
+ DCHECK(crypto_key_header != message.data.end());
+
+ EncryptionHeaderIterator encryption_header_iterator(
+ encryption_header->second.begin(), encryption_header->second.end());
+ if (!encryption_header_iterator.GetNext()) {
+ DLOG(ERROR) << "Unable to parse the value of the Encryption header";
+ std::move(callback).Run(GCMDecryptionResult::INVALID_ENCRYPTION_HEADER,
+ CreateMessageWithId(message.message_id));
+ return;
+ }
+
+ if (encryption_header_iterator.salt().size() !=
+ GCMMessageCryptographer::kSaltSize) {
+ DLOG(ERROR) << "Invalid values supplied in the Encryption header";
+ std::move(callback).Run(GCMDecryptionResult::INVALID_ENCRYPTION_HEADER,
+ CreateMessageWithId(message.message_id));
+ return;
+ }
+
+ salt = encryption_header_iterator.salt();
+ record_size = encryption_header_iterator.rs();
+
+ CryptoKeyHeaderIterator crypto_key_header_iterator(
+ crypto_key_header->second.begin(), crypto_key_header->second.end());
+ if (!crypto_key_header_iterator.GetNext()) {
+ DLOG(ERROR) << "Unable to parse the value of the Crypto-Key header";
+ std::move(callback).Run(GCMDecryptionResult::INVALID_CRYPTO_KEY_HEADER,
+ CreateMessageWithId(message.message_id));
+ return;
+ }
+
+ // Ignore values that don't include the "dh" property. When using VAPID, it
+ // is valid for the application server to supply multiple values.
+ while (crypto_key_header_iterator.dh().empty() &&
+ crypto_key_header_iterator.GetNext()) {
+ }
+
+ bool valid_crypto_key_header = false;
+
+ if (!crypto_key_header_iterator.dh().empty()) {
+ public_key = crypto_key_header_iterator.dh();
+ valid_crypto_key_header = true;
+
+ // Guard against the "dh" property being included more than once.
+ while (crypto_key_header_iterator.GetNext()) {
+ if (crypto_key_header_iterator.dh().empty())
+ continue;
+
+ valid_crypto_key_header = false;
+ break;
+ }
+ }
+
+ if (!valid_crypto_key_header) {
+ DLOG(ERROR) << "Invalid values supplied in the Crypto-Key header";
+ std::move(callback).Run(GCMDecryptionResult::INVALID_CRYPTO_KEY_HEADER,
+ CreateMessageWithId(message.message_id));
+ return;
+ }
+
+ ciphertext = message.raw_data;
+ version = GCMMessageCryptographer::Version::DRAFT_03;
+ }
+
+ // Use |fallback_to_empty_authorized_entity|, since this message might have
+ // been sent to either an InstanceID token or a non-InstanceID registration.
+ key_store_->GetKeys(
+ app_id, message.sender_id /* authorized_entity */,
+ true /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMEncryptionProvider::DecryptMessageWithKey,
+ weak_ptr_factory_.GetWeakPtr(), message.message_id,
+ message.collapse_key, message.sender_id, std::move(salt),
+ std::move(public_key), record_size, std::move(ciphertext),
+ version, std::move(callback)));
+} // namespace gcm
+
+void GCMEncryptionProvider::EncryptMessage(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& p256dh,
+ const std::string& auth_secret,
+ const std::string& message,
+ EncryptMessageCallback callback) {
+ DCHECK(key_store_);
+ key_store_->GetKeys(
+ app_id, authorized_entity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMEncryptionProvider::EncryptMessageWithKey,
+ weak_ptr_factory_.GetWeakPtr(), app_id, authorized_entity,
+ p256dh, auth_secret, message, std::move(callback)));
+}
+
+void GCMEncryptionProvider::DidCreateEncryptionInfo(
+ EncryptionInfoCallback callback,
+ std::unique_ptr<crypto::ECPrivateKey> key,
+ const std::string& auth_secret) {
+ if (!key) {
+ std::move(callback).Run(std::string() /* p256dh */,
+ std::string() /* auth_secret */);
+ return;
+ }
+
+ std::string public_key;
+ const bool success = GetRawPublicKey(*key, &public_key);
+ DCHECK(success);
+ std::move(callback).Run(public_key, auth_secret);
+}
+
+void GCMEncryptionProvider::DecryptMessageWithKey(
+ const std::string& message_id,
+ const std::string& collapse_key,
+ const std::string& sender_id,
+ const std::string& salt,
+ const std::string& public_key,
+ uint32_t record_size,
+ const std::string& ciphertext,
+ GCMMessageCryptographer::Version version,
+ DecryptMessageCallback callback,
+ std::unique_ptr<crypto::ECPrivateKey> key,
+ const std::string& auth_secret) {
+ if (!key) {
+ DLOG(ERROR) << "Unable to retrieve the keys for the incoming message.";
+ std::move(callback).Run(GCMDecryptionResult::NO_KEYS,
+ CreateMessageWithId(message_id));
+ return;
+ }
+
+ std::string shared_secret;
+ if (!ComputeSharedP256Secret(*key, public_key, &shared_secret)) {
+ DLOG(ERROR) << "Unable to calculate the shared secret.";
+ std::move(callback).Run(GCMDecryptionResult::INVALID_SHARED_SECRET,
+ CreateMessageWithId(message_id));
+ return;
+ }
+
+ std::string plaintext;
+
+ GCMMessageCryptographer cryptographer(version);
+
+ std::string exported_public_key;
+ const bool success = GetRawPublicKey(*key, &exported_public_key);
+ DCHECK(success);
+ if (!cryptographer.Decrypt(exported_public_key, public_key, shared_secret,
+ auth_secret, salt, ciphertext, record_size,
+ &plaintext)) {
+ DLOG(ERROR) << "Unable to decrypt the incoming data.";
+ std::move(callback).Run(GCMDecryptionResult::INVALID_PAYLOAD,
+ CreateMessageWithId(message_id));
+ return;
+ }
+
+ IncomingMessage decrypted_message;
+ decrypted_message.message_id = message_id;
+ decrypted_message.collapse_key = collapse_key;
+ decrypted_message.sender_id = sender_id;
+ decrypted_message.raw_data.swap(plaintext);
+ decrypted_message.decrypted = true;
+
+ // There must be no data associated with the decrypted message at this point,
+ // to make sure that we don't end up in an infinite decryption loop.
+ DCHECK_EQ(0u, decrypted_message.data.size());
+
+ std::move(callback).Run(version == GCMMessageCryptographer::Version::DRAFT_03
+ ? GCMDecryptionResult::DECRYPTED_DRAFT_03
+ : GCMDecryptionResult::DECRYPTED_DRAFT_08,
+ std::move(decrypted_message));
+}
+
+void GCMEncryptionProvider::EncryptMessageWithKey(
+ const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& p256dh,
+ const std::string& auth_secret,
+ const std::string& message,
+ EncryptMessageCallback callback,
+ std::unique_ptr<crypto::ECPrivateKey> key,
+ const std::string& sender_auth_secret) {
+ if (!key) {
+ DLOG(ERROR) << "Unable to retrieve the keys for the outgoing message.";
+ std::move(callback).Run(GCMEncryptionResult::NO_KEYS, std::string());
+ return;
+ }
+
+ // Creates a cryptographically secure salt of |salt_size| octets in size,
+ // and calculate the shared secret for the message.
+ std::string salt;
+ crypto::RandBytes(base::WriteInto(&salt, 16 + 1), 16);
+
+ std::string shared_secret;
+ if (!ComputeSharedP256Secret(*key, p256dh, &shared_secret)) {
+ DLOG(ERROR) << "Unable to calculate the shared secret.";
+ std::move(callback).Run(GCMEncryptionResult::INVALID_SHARED_SECRET,
+ std::string());
+ return;
+ }
+
+ size_t record_size;
+ std::string ciphertext;
+
+ GCMMessageCryptographer cryptographer(
+ GCMMessageCryptographer::Version::DRAFT_08);
+
+ std::string sender_public_key;
+ bool success = GetRawPublicKey(*key, &sender_public_key);
+ DCHECK(success);
+ if (!cryptographer.Encrypt(p256dh, sender_public_key, shared_secret,
+ auth_secret, salt, message, &record_size,
+ &ciphertext)) {
+ DLOG(ERROR) << "Unable to encrypt the incoming data.";
+ std::move(callback).Run(GCMEncryptionResult::ENCRYPTION_FAILED,
+ std::string());
+ return;
+ }
+
+ // Construct encryption header.
+ uint32_t rs = record_size;
+ char rs_buf[sizeof(rs)];
+ base::WriteBigEndian(rs_buf, rs);
+ std::string rs_str(std::begin(rs_buf), std::end(rs_buf));
+
+ uint8_t key_length = sender_public_key.size();
+ char key_length_buf[sizeof(key_length)];
+ base::WriteBigEndian(key_length_buf, key_length);
+ std::string key_length_str(std::begin(key_length_buf),
+ std::end(key_length_buf));
+
+ std::string payload = base::StrCat(
+ {salt, rs_str, key_length_str, sender_public_key, ciphertext});
+ std::move(callback).Run(GCMEncryptionResult::ENCRYPTED_DRAFT_08,
+ std::move(payload));
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/crypto/gcm_encryption_provider.h b/chromium/components/gcm_driver/crypto/gcm_encryption_provider.h
new file mode 100644
index 00000000000..bdb6cd3d15e
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/gcm_encryption_provider.h
@@ -0,0 +1,157 @@
+// Copyright 2015 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 COMPONENTS_GCM_DRIVER_CRYPTO_GCM_ENCRYPTION_PROVIDER_H_
+#define COMPONENTS_GCM_DRIVER_CRYPTO_GCM_ENCRYPTION_PROVIDER_H_
+
+#include <stdint.h>
+
+#include <memory>
+#include <string>
+
+#include "base/callback_forward.h"
+#include "base/gtest_prod_util.h"
+#include "base/memory/weak_ptr.h"
+#include "components/gcm_driver/crypto/gcm_message_cryptographer.h"
+
+namespace base {
+class FilePath;
+class SequencedTaskRunner;
+} // namespace base
+
+namespace crypto {
+class ECPrivateKey;
+} // namespace crypto
+
+namespace gcm {
+
+enum class GCMDecryptionResult;
+enum class GCMEncryptionResult;
+class GCMKeyStore;
+struct IncomingMessage;
+
+// Provider that enables the GCM Driver to deal with encryption key management
+// and decryption of incoming messages.
+class GCMEncryptionProvider {
+ public:
+ // Callback to be invoked when the public key and auth secret are available.
+ using EncryptionInfoCallback =
+ base::OnceCallback<void(std::string p256dh, std::string auth_secret)>;
+
+ // Callback to be invoked when a message may have been decrypted, as indicated
+ // by the |result|. The |message| contains the dispatchable message in success
+ // cases, or will be initialized to an empty, default state for failure.
+ using DecryptMessageCallback =
+ base::OnceCallback<void(GCMDecryptionResult result,
+ IncomingMessage message)>;
+
+ // Callback to be invoked when a message may have been encrypted, as indicated
+ // by the |result|. The |message| contains the dispatchable message in success
+ // cases, or will be initialized to an empty, default state for failure.
+ using EncryptMessageCallback =
+ base::OnceCallback<void(GCMEncryptionResult result, std::string message)>;
+
+ static const char kContentEncodingProperty[];
+
+ // Content coding name defined by ietf-httpbis-encryption-encoding.
+ static const char kContentCodingAes128Gcm[];
+
+ GCMEncryptionProvider();
+
+ GCMEncryptionProvider(const GCMEncryptionProvider&) = delete;
+ GCMEncryptionProvider& operator=(const GCMEncryptionProvider&) = delete;
+
+ ~GCMEncryptionProvider();
+
+ // Initializes the encryption provider with the |store_path| and the
+ // |blocking_task_runner|. Done separately from the constructor in order to
+ // avoid needing a blocking task runner for anything using GCMDriver.
+ void Init(
+ const base::FilePath& store_path,
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner);
+
+ // Retrieves the public key and authentication secret associated with the
+ // |app_id| + |authorized_entity| pair. Will create this info if necessary.
+ // |authorized_entity| should be the InstanceID token's authorized entity, or
+ // "" for non-InstanceID GCM registrations.
+ void GetEncryptionInfo(const std::string& app_id,
+ const std::string& authorized_entity,
+ EncryptionInfoCallback callback);
+
+ // Removes all encryption information associated with the |app_id| +
+ // |authorized_entity| pair, then invokes |callback|. |authorized_entity|
+ // should be the InstanceID token's authorized entity, or "*" to remove for
+ // all InstanceID tokens, or "" for non-InstanceID GCM registrations.
+ void RemoveEncryptionInfo(const std::string& app_id,
+ const std::string& authorized_entity,
+ base::OnceClosure callback);
+
+ // Determines whether |message| contains encrypted content.
+ bool IsEncryptedMessage(const IncomingMessage& message) const;
+
+ // Attempts to decrypt the |message|. If the |message| is not encrypted, the
+ // |callback| will be invoked immediately. Otherwise |callback| will be called
+ // asynchronously when |message| has been decrypted. A dispatchable message
+ // will be used in case of success, an empty message in case of failure.
+ void DecryptMessage(const std::string& app_id,
+ const IncomingMessage& message,
+ DecryptMessageCallback callback);
+
+ // Attempts to encrypt the |message| using draft-ietf-webpush-encryption-08
+ // scheme. |callback| will be called asynchronously when |message| has been
+ // encrypted. A dispatchable message will be used in case of success, an empty
+ // message in case of failure.
+ void EncryptMessage(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& p256dh,
+ const std::string& auth_secret,
+ const std::string& message,
+ EncryptMessageCallback callback);
+
+ private:
+ friend class GCMEncryptionProviderTest;
+ FRIEND_TEST_ALL_PREFIXES(GCMEncryptionProviderTest,
+ EncryptionRoundTripGCMRegistration);
+ FRIEND_TEST_ALL_PREFIXES(GCMEncryptionProviderTest,
+ EncryptionRoundTripInstanceIDToken);
+
+ void DidGetEncryptionInfo(const std::string& app_id,
+ const std::string& authorized_entity,
+ EncryptionInfoCallback callback,
+ std::unique_ptr<crypto::ECPrivateKey> key,
+ const std::string& auth_secret);
+
+ void DidCreateEncryptionInfo(EncryptionInfoCallback callback,
+ std::unique_ptr<crypto::ECPrivateKey> key,
+ const std::string& auth_secret);
+
+ void DecryptMessageWithKey(const std::string& message_id,
+ const std::string& collapse_key,
+ const std::string& sender_id,
+ const std::string& salt,
+ const std::string& public_key,
+ uint32_t record_size,
+ const std::string& ciphertext,
+ GCMMessageCryptographer::Version version,
+ DecryptMessageCallback callback,
+ std::unique_ptr<crypto::ECPrivateKey> key,
+ const std::string& auth_secret);
+
+ void EncryptMessageWithKey(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& p256dh,
+ const std::string& auth_secret,
+ const std::string& message,
+ EncryptMessageCallback callback,
+ std::unique_ptr<crypto::ECPrivateKey> key,
+ const std::string& sender_auth_secret);
+
+ std::unique_ptr<GCMKeyStore> key_store_;
+
+ base::WeakPtrFactory<GCMEncryptionProvider> weak_ptr_factory_{this};
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_CRYPTO_GCM_ENCRYPTION_PROVIDER_H_
diff --git a/chromium/components/gcm_driver/crypto/gcm_encryption_provider_unittest.cc b/chromium/components/gcm_driver/crypto/gcm_encryption_provider_unittest.cc
new file mode 100644
index 00000000000..b4fe8b2f6dd
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/gcm_encryption_provider_unittest.cc
@@ -0,0 +1,672 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/crypto/gcm_encryption_provider.h"
+
+#include <stddef.h>
+
+#include <memory>
+#include <sstream>
+#include <string>
+
+#include "base/base64.h"
+#include "base/base64url.h"
+#include "base/big_endian.h"
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/run_loop.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_piece.h"
+#include "base/strings/string_util.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/task_environment.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "components/gcm_driver/common/gcm_message.h"
+#include "components/gcm_driver/crypto/gcm_decryption_result.h"
+#include "components/gcm_driver/crypto/gcm_encryption_result.h"
+#include "components/gcm_driver/crypto/gcm_key_store.h"
+#include "components/gcm_driver/crypto/gcm_message_cryptographer.h"
+#include "components/gcm_driver/crypto/p256_key_util.h"
+#include "crypto/random.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+namespace {
+
+const char kExampleAppId[] = "my-app-id";
+const char kExampleAuthorizedEntity[] = "my-sender-id";
+const char kExampleMessage[] = "Hello, world, this is the GCM Driver!";
+
+const char kValidEncryptionHeader[] =
+ "keyid=foo;salt=MTIzNDU2Nzg5MDEyMzQ1Ng;rs=1024";
+const char kInvalidEncryptionHeader[] = "keyid";
+
+const char kValidCryptoKeyHeader[] =
+ "keyid=foo;dh=BL_UGhfudEkXMUd4U4-D4nP5KHxKjQHsW6j88ybbehXM7fqi1OMFefDUEi0eJ"
+ "vsKfyVBWYkQjH-lSPJKxjAyslg";
+const char kValidThreeValueCryptoKeyHeader[] =
+ "keyid=foo,keyid=bar,keyid=baz;dh=BL_UGhfudEkXMUd4U4-D4nP5KHxKjQHsW6j88ybbe"
+ "hXM7fqi1OMFefDUEi0eJvsKfyVBWYkQjH-lSPJKxjAyslg";
+
+const char kInvalidCryptoKeyHeader[] = "keyid";
+const char kInvalidThreeValueCryptoKeyHeader[] =
+ "keyid=foo,dh=BL_UGhfudEkXMUd4U4-D4nP5KHxKjQHsW6j88ybbehXM7fqi1OMFefDUEi0eJ"
+ "vsKfyVBWYkQjH-lSPJKxjAyslg,keyid=baz,dh=BL_UGhfudEkXMUd4U4-D4nP5KHxKjQHsW6"
+ "j88ybbehXM7fqi1OMFefDUEi0eJvsKfyVBWYkQjH-lSPJKxjAyslg";
+
+} // namespace
+
+using ECPrivateKeyUniquePtr = std::unique_ptr<crypto::ECPrivateKey>;
+
+class GCMEncryptionProviderTest : public ::testing::Test {
+ public:
+ void SetUp() override {
+ ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
+
+ encryption_provider_ = std::make_unique<GCMEncryptionProvider>();
+ encryption_provider_->Init(scoped_temp_dir_.GetPath(),
+ base::ThreadTaskRunnerHandle::Get());
+ }
+
+ void TearDown() override {
+ encryption_provider_.reset();
+
+ // |encryption_provider_| owns a ProtoDatabase whose destructor deletes
+ // the underlying LevelDB database on the task runner.
+ base::RunLoop().RunUntilIdle();
+ }
+
+ // To be used as a callback for GCMEncryptionProvider::GetEncryptionInfo().
+ void DidGetEncryptionInfo(std::string* p256dh_out,
+ std::string* auth_secret_out,
+ std::string p256dh,
+ std::string auth_secret) {
+ *p256dh_out = std::move(p256dh);
+ *auth_secret_out = std::move(auth_secret);
+ }
+
+ // To be used as a callback for GCMKeyStore::{GetKeys,CreateKeys}.
+ void HandleKeysCallback(ECPrivateKeyUniquePtr* key_out,
+ std::string* auth_secret_out,
+ ECPrivateKeyUniquePtr key,
+ const std::string& auth_secret) {
+ *key_out = std::move(key);
+ *auth_secret_out = auth_secret;
+ }
+
+ protected:
+ // Decrypts the |message| and then synchronously waits until either the
+ // success or failure callbacks has been invoked.
+ void Decrypt(const IncomingMessage& message) {
+ encryption_provider_->DecryptMessage(
+ kExampleAppId, message,
+ base::BindOnce(&GCMEncryptionProviderTest::DidDecryptMessage,
+ base::Unretained(this)));
+
+ // The encryption keys will be read asynchronously.
+ base::RunLoop().RunUntilIdle();
+ }
+
+ // Encrypts the |message| and then synchronously waits until either the
+ // success or failure callbacks has been invoked.
+ void Encrypt(const std::string& authorized_entity,
+ const std::string& p256dh,
+ const std::string& auth_secret,
+ const std::string& message) {
+ encryption_provider_->EncryptMessage(
+ kExampleAppId, authorized_entity, p256dh, auth_secret, message,
+ base::BindOnce(&GCMEncryptionProviderTest::DidEncryptMessage,
+ base::Unretained(this)));
+
+ // The encryption keys will be read asynchronously.
+ base::RunLoop().RunUntilIdle();
+ }
+
+ // Checks that the underlying key store has a key for the |kExampleAppId| +
+ // authorized entity key if and only if |should_have_key| is true. Must wrap
+ // with ASSERT/EXPECT_NO_FATAL_FAILURE.
+ void CheckHasKey(const std::string& authorized_entity, bool should_have_key) {
+ ECPrivateKeyUniquePtr key;
+ std::string auth_secret;
+ encryption_provider()->key_store_->GetKeys(
+ kExampleAppId, authorized_entity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMEncryptionProviderTest::HandleKeysCallback,
+ base::Unretained(this), &key, &auth_secret));
+
+ base::RunLoop().RunUntilIdle();
+
+ if (should_have_key) {
+ ASSERT_TRUE(key);
+ std::string private_key, public_key;
+ ASSERT_TRUE(GetRawPrivateKey(*key, &private_key));
+ ASSERT_TRUE(GetRawPublicKey(*key, &public_key));
+ ASSERT_GT(public_key.size(), 0u);
+ ASSERT_GT(private_key.size(), 0u);
+ ASSERT_GT(auth_secret.size(), 0u);
+ } else {
+ ASSERT_FALSE(key);
+ ASSERT_EQ(0u, auth_secret.size());
+ }
+ }
+
+ // Returns the result of the previous decryption operation.
+ GCMDecryptionResult decryption_result() { return decryption_result_; }
+
+ // Returns the result of the previous encryption operation.
+ GCMEncryptionResult encryption_result() { return encryption_result_; }
+
+ // Returns the message resulting from the previous decryption operation.
+ const IncomingMessage& decrypted_message() { return decrypted_message_; }
+
+ // Returns the message resulting from the previous encryption operation.
+ const std::string& encrypted_message() { return encrypted_message_; }
+
+ GCMEncryptionProvider* encryption_provider() {
+ return encryption_provider_.get();
+ }
+
+ // Performs a full round-trip test of the encryption feature. Must wrap this
+ // in ASSERT_NO_FATAL_FAILURE.
+ void TestEncryptionRoundTrip(const std::string& app_id,
+ const std::string& authorized_entity,
+ GCMMessageCryptographer::Version version,
+ bool use_internal_raw_data_for_draft08 = false);
+
+ // Performs a test encryption feature without creating proper keys. Must wrap
+ // this in ASSERT_NO_FATAL_FAILURE.
+ void TestEncryptionNoKeys(const std::string& app_id,
+ const std::string& authorized_entity);
+
+ private:
+ void DidDecryptMessage(GCMDecryptionResult result, IncomingMessage message) {
+ decryption_result_ = result;
+ decrypted_message_ = std::move(message);
+ }
+
+ void DidEncryptMessage(GCMEncryptionResult result, std::string message) {
+ encryption_result_ = result;
+ encrypted_message_ = std::move(message);
+ }
+
+ base::test::SingleThreadTaskEnvironment task_environment_;
+ base::ScopedTempDir scoped_temp_dir_;
+ base::HistogramTester histogram_tester_;
+
+ std::unique_ptr<GCMEncryptionProvider> encryption_provider_;
+
+ GCMDecryptionResult decryption_result_ = GCMDecryptionResult::UNENCRYPTED;
+ GCMEncryptionResult encryption_result_ =
+ GCMEncryptionResult::ENCRYPTION_FAILED;
+
+ IncomingMessage decrypted_message_;
+ std::string encrypted_message_;
+};
+
+TEST_F(GCMEncryptionProviderTest, IsEncryptedMessage) {
+ // Both the Encryption and Encryption-Key headers must be present, and the raw
+ // data must be non-empty for a message to be considered encrypted.
+
+ IncomingMessage empty_message;
+ EXPECT_FALSE(encryption_provider()->IsEncryptedMessage(empty_message));
+
+ IncomingMessage single_header_message;
+ single_header_message.data["encryption"] = "";
+ EXPECT_FALSE(
+ encryption_provider()->IsEncryptedMessage(single_header_message));
+
+ IncomingMessage double_header_message;
+ double_header_message.data["encryption"] = "";
+ double_header_message.data["crypto-key"] = "";
+ EXPECT_FALSE(
+ encryption_provider()->IsEncryptedMessage(double_header_message));
+
+ IncomingMessage double_header_with_data_message;
+ double_header_with_data_message.data["encryption"] = "";
+ double_header_with_data_message.data["crypto-key"] = "";
+ double_header_with_data_message.raw_data = "foo";
+ EXPECT_TRUE(encryption_provider()->IsEncryptedMessage(
+ double_header_with_data_message));
+
+ IncomingMessage draft08_message;
+ draft08_message.data["content-encoding"] = "aes128gcm";
+ draft08_message.raw_data = "foo";
+ EXPECT_TRUE(encryption_provider()->IsEncryptedMessage(draft08_message));
+}
+
+TEST_F(GCMEncryptionProviderTest, VerifiesEncryptionHeaderParsing) {
+ // The Encryption header must be parsable and contain valid values.
+ // Note that this is more extensively tested in EncryptionHeaderParsersTest.
+
+ IncomingMessage invalid_message;
+ invalid_message.data["encryption"] = kInvalidEncryptionHeader;
+ invalid_message.data["crypto-key"] = kValidCryptoKeyHeader;
+ invalid_message.raw_data = "foo";
+
+ ASSERT_NO_FATAL_FAILURE(Decrypt(invalid_message));
+ EXPECT_EQ(GCMDecryptionResult::INVALID_ENCRYPTION_HEADER,
+ decryption_result());
+
+ IncomingMessage valid_message;
+ valid_message.data["encryption"] = kValidEncryptionHeader;
+ valid_message.data["crypto-key"] = kInvalidCryptoKeyHeader;
+ valid_message.raw_data = "foo";
+
+ ASSERT_NO_FATAL_FAILURE(Decrypt(valid_message));
+ EXPECT_NE(GCMDecryptionResult::INVALID_ENCRYPTION_HEADER,
+ decryption_result());
+}
+
+TEST_F(GCMEncryptionProviderTest, VerifiesCryptoKeyHeaderParsing) {
+ // The Crypto-Key header must be parsable and contain valid values.
+ // Note that this is more extensively tested in EncryptionHeaderParsersTest.
+
+ IncomingMessage invalid_message;
+ invalid_message.data["encryption"] = kValidEncryptionHeader;
+ invalid_message.data["crypto-key"] = kInvalidCryptoKeyHeader;
+ invalid_message.raw_data = "foo";
+
+ ASSERT_NO_FATAL_FAILURE(Decrypt(invalid_message));
+ EXPECT_EQ(GCMDecryptionResult::INVALID_CRYPTO_KEY_HEADER,
+ decryption_result());
+
+ IncomingMessage valid_message;
+ valid_message.data["encryption"] = kValidEncryptionHeader;
+ valid_message.data["crypto-key"] = kValidCryptoKeyHeader;
+ valid_message.raw_data = "foo";
+
+ ASSERT_NO_FATAL_FAILURE(Decrypt(valid_message));
+ EXPECT_NE(GCMDecryptionResult::INVALID_CRYPTO_KEY_HEADER,
+ decryption_result());
+}
+
+TEST_F(GCMEncryptionProviderTest, VerifiesCryptoKeyHeaderParsingThirdValue) {
+ // The Crypto-Key header must be parsable and contain valid values, in which
+ // values will be ignored unless they contain a "dh" property.
+
+ IncomingMessage valid_message;
+ valid_message.data["encryption"] = kValidEncryptionHeader;
+ valid_message.data["crypto-key"] = kValidThreeValueCryptoKeyHeader;
+ valid_message.raw_data = "foo";
+
+ ASSERT_NO_FATAL_FAILURE(Decrypt(valid_message));
+ EXPECT_NE(GCMDecryptionResult::INVALID_CRYPTO_KEY_HEADER,
+ decryption_result());
+}
+
+TEST_F(GCMEncryptionProviderTest, VerifiesCryptoKeyHeaderSingleDhEntry) {
+ // The Crypto-Key header must include at most one value that contains the
+ // "dh" property. Having more than once occurrence is forbidden.
+
+ IncomingMessage valid_message;
+ valid_message.data["encryption"] = kValidEncryptionHeader;
+ valid_message.data["crypto-key"] = kInvalidThreeValueCryptoKeyHeader;
+ valid_message.raw_data = "foo";
+
+ ASSERT_NO_FATAL_FAILURE(Decrypt(valid_message));
+ EXPECT_EQ(GCMDecryptionResult::INVALID_CRYPTO_KEY_HEADER,
+ decryption_result());
+}
+
+TEST_F(GCMEncryptionProviderTest, VerifiesExistingKeys) {
+ // When both headers are valid, the encryption keys still must be known to
+ // the GCM key store before the message can be decrypted.
+
+ IncomingMessage message;
+ message.data["encryption"] = kValidEncryptionHeader;
+ message.data["crypto-key"] = kValidCryptoKeyHeader;
+ message.raw_data = "foo";
+
+ ASSERT_NO_FATAL_FAILURE(Decrypt(message));
+ EXPECT_EQ(GCMDecryptionResult::NO_KEYS, decryption_result());
+
+ std::string public_key, auth_secret;
+ encryption_provider()->GetEncryptionInfo(
+ kExampleAppId, "" /* empty authorized entity for non-InstanceID */,
+ base::BindOnce(&GCMEncryptionProviderTest::DidGetEncryptionInfo,
+ base::Unretained(this), &public_key, &auth_secret));
+
+ // Getting (or creating) the public key will be done asynchronously.
+ base::RunLoop().RunUntilIdle();
+
+ ASSERT_GT(public_key.size(), 0u);
+ ASSERT_GT(auth_secret.size(), 0u);
+
+ ASSERT_NO_FATAL_FAILURE(Decrypt(message));
+ EXPECT_NE(GCMDecryptionResult::NO_KEYS, decryption_result());
+}
+
+TEST_F(GCMEncryptionProviderTest, VerifiesKeyRemovalGCMRegistration) {
+ // Removing encryption info for an InstanceID token shouldn't affect a
+ // non-InstanceID GCM registration.
+
+ // Non-InstanceID callers pass an empty string for authorized_entity.
+ std::string authorized_entity_gcm;
+ std::string authorized_entity_1 = kExampleAuthorizedEntity + std::string("1");
+ std::string authorized_entity_2 = kExampleAuthorizedEntity + std::string("2");
+
+ // Should create encryption info.
+ std::string public_key, auth_secret;
+ encryption_provider()->GetEncryptionInfo(
+ kExampleAppId, authorized_entity_gcm,
+ base::BindOnce(&GCMEncryptionProviderTest::DidGetEncryptionInfo,
+ base::Unretained(this), &public_key, &auth_secret));
+
+ base::RunLoop().RunUntilIdle();
+
+ // Should get encryption info created above.
+ std::string read_public_key, read_auth_secret;
+ encryption_provider()->GetEncryptionInfo(
+ kExampleAppId, authorized_entity_gcm,
+ base::BindOnce(&GCMEncryptionProviderTest::DidGetEncryptionInfo,
+ base::Unretained(this), &read_public_key,
+ &read_auth_secret));
+
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_GT(public_key.size(), 0u);
+ EXPECT_GT(auth_secret.size(), 0u);
+ EXPECT_EQ(public_key, read_public_key);
+ EXPECT_EQ(auth_secret, read_auth_secret);
+
+ ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_gcm, true));
+
+ encryption_provider()->RemoveEncryptionInfo(
+ kExampleAppId, authorized_entity_1, base::DoNothing());
+
+ base::RunLoop().RunUntilIdle();
+
+ ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_gcm, true));
+
+ encryption_provider()->RemoveEncryptionInfo(kExampleAppId, "*",
+ base::DoNothing());
+
+ base::RunLoop().RunUntilIdle();
+
+ ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_gcm, true));
+
+ encryption_provider()->RemoveEncryptionInfo(
+ kExampleAppId, authorized_entity_gcm, base::DoNothing());
+
+ base::RunLoop().RunUntilIdle();
+
+ ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_gcm, false));
+}
+
+TEST_F(GCMEncryptionProviderTest, VerifiesKeyRemovalInstanceIDToken) {
+ // Removing encryption info for a non-InstanceID GCM registration shouldn't
+ // affect an InstanceID token.
+
+ // Non-InstanceID callers pass an empty string for authorized_entity.
+ std::string authorized_entity_gcm;
+ std::string authorized_entity_1 = kExampleAuthorizedEntity + std::string("1");
+ std::string authorized_entity_2 = kExampleAuthorizedEntity + std::string("2");
+
+ std::string public_key_1, auth_secret_1;
+ encryption_provider()->GetEncryptionInfo(
+ kExampleAppId, authorized_entity_1,
+ base::BindOnce(&GCMEncryptionProviderTest::DidGetEncryptionInfo,
+ base::Unretained(this), &public_key_1, &auth_secret_1));
+
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_GT(public_key_1.size(), 0u);
+ EXPECT_GT(auth_secret_1.size(), 0u);
+
+ ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_1, true));
+ ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_2, false));
+
+ std::string public_key_2, auth_secret_2;
+ encryption_provider()->GetEncryptionInfo(
+ kExampleAppId, authorized_entity_2,
+ base::BindOnce(&GCMEncryptionProviderTest::DidGetEncryptionInfo,
+ base::Unretained(this), &public_key_2, &auth_secret_2));
+
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_GT(public_key_2.size(), 0u);
+ EXPECT_GT(auth_secret_2.size(), 0u);
+ EXPECT_NE(public_key_1, public_key_2);
+ EXPECT_NE(auth_secret_1, auth_secret_2);
+
+ ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_1, true));
+ ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_2, true));
+
+ std::string read_public_key_1, read_auth_secret_1;
+ encryption_provider()->GetEncryptionInfo(
+ kExampleAppId, authorized_entity_1,
+ base::BindOnce(&GCMEncryptionProviderTest::DidGetEncryptionInfo,
+ base::Unretained(this), &read_public_key_1,
+ &read_auth_secret_1));
+
+ base::RunLoop().RunUntilIdle();
+
+ // Should have returned existing info for authorized_entity_1.
+ EXPECT_EQ(public_key_1, read_public_key_1);
+ EXPECT_EQ(auth_secret_1, read_auth_secret_1);
+
+ encryption_provider()->RemoveEncryptionInfo(
+ kExampleAppId, authorized_entity_gcm, base::DoNothing());
+
+ base::RunLoop().RunUntilIdle();
+
+ ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_1, true));
+ ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_2, true));
+
+ encryption_provider()->RemoveEncryptionInfo(
+ kExampleAppId, authorized_entity_1, base::DoNothing());
+
+ base::RunLoop().RunUntilIdle();
+
+ ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_1, false));
+ ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_2, true));
+
+ std::string public_key_1_refreshed, auth_secret_1_refreshed;
+ encryption_provider()->GetEncryptionInfo(
+ kExampleAppId, authorized_entity_1,
+ base::BindOnce(&GCMEncryptionProviderTest::DidGetEncryptionInfo,
+ base::Unretained(this), &public_key_1_refreshed,
+ &auth_secret_1_refreshed));
+
+ base::RunLoop().RunUntilIdle();
+
+ // Since the info was removed, GetEncryptionInfo should have created new info.
+ EXPECT_GT(public_key_1_refreshed.size(), 0u);
+ EXPECT_GT(auth_secret_1_refreshed.size(), 0u);
+ EXPECT_NE(public_key_1, public_key_1_refreshed);
+ EXPECT_NE(auth_secret_1, auth_secret_1_refreshed);
+ EXPECT_NE(public_key_2, public_key_1_refreshed);
+ EXPECT_NE(auth_secret_2, auth_secret_1_refreshed);
+
+ ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_1, true));
+ ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_2, true));
+
+ encryption_provider()->RemoveEncryptionInfo(kExampleAppId, "*",
+ base::DoNothing());
+
+ base::RunLoop().RunUntilIdle();
+
+ ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_1, false));
+ ASSERT_NO_FATAL_FAILURE(CheckHasKey(authorized_entity_2, false));
+}
+
+void GCMEncryptionProviderTest::TestEncryptionRoundTrip(
+ const std::string& app_id,
+ const std::string& authorized_entity,
+ GCMMessageCryptographer::Version version,
+ bool use_internal_raw_data_for_draft08) {
+ // Performs a full round-trip of the encryption feature, including getting a
+ // public/private key-key and performing the cryptographic operations. This
+ // is more of an integration test than a unit test.
+
+ ECPrivateKeyUniquePtr key, server_key;
+ std::string auth_secret, server_authentication;
+
+ // Retrieve the public/private key-key immediately from the key store, given
+ // that the GCMEncryptionProvider will only share the public key with users.
+ // Also create a second key, which will act as the server's keys.
+ encryption_provider()->key_store_->CreateKeys(
+ app_id, authorized_entity,
+ base::BindOnce(&GCMEncryptionProviderTest::HandleKeysCallback,
+ base::Unretained(this), &key, &auth_secret));
+
+ encryption_provider()->key_store_->CreateKeys(
+ "server-" + app_id, authorized_entity,
+ base::BindOnce(&GCMEncryptionProviderTest::HandleKeysCallback,
+ base::Unretained(this), &server_key,
+ &server_authentication));
+
+ // Creating the public keys will be done asynchronously.
+ base::RunLoop().RunUntilIdle();
+
+ std::string public_key, server_public_key;
+ ASSERT_TRUE(GetRawPublicKey(*key, &public_key));
+ ASSERT_TRUE(GetRawPublicKey(*server_key, &server_public_key));
+ ASSERT_GT(public_key.size(), 0u);
+ ASSERT_GT(server_public_key.size(), 0u);
+
+ std::string private_key, server_private_key;
+ ASSERT_TRUE(GetRawPublicKey(*key, &private_key));
+ ASSERT_TRUE(GetRawPublicKey(*server_key, &server_private_key));
+ ASSERT_GT(private_key.size(), 0u);
+ ASSERT_GT(server_private_key.size(), 0u);
+
+ IncomingMessage message;
+ message.sender_id = authorized_entity;
+
+ switch (version) {
+ case GCMMessageCryptographer::Version::DRAFT_03: {
+ std::string salt;
+
+ // Creates a cryptographically secure salt of |salt_size| octets in size,
+ // and calculate the shared secret for the message.
+ crypto::RandBytes(base::WriteInto(&salt, 16 + 1), 16);
+
+ std::string shared_secret;
+ ASSERT_TRUE(
+ ComputeSharedP256Secret(*key, server_public_key, &shared_secret));
+
+ size_t record_size;
+
+ // Encrypts the |kExampleMessage| using the generated shared key and the
+ // random |salt|, storing the result in |record_size| and the message.
+ GCMMessageCryptographer cryptographer(version);
+
+ std::string ciphertext;
+ ASSERT_TRUE(cryptographer.Encrypt(
+ public_key, server_public_key, shared_secret, auth_secret, salt,
+ kExampleMessage, &record_size, &ciphertext));
+
+ std::string encoded_salt, encoded_key;
+
+ // Compile the incoming GCM message, including the required headers.
+ base::Base64UrlEncode(salt, base::Base64UrlEncodePolicy::INCLUDE_PADDING,
+ &encoded_salt);
+ base::Base64UrlEncode(server_public_key,
+ base::Base64UrlEncodePolicy::INCLUDE_PADDING,
+ &encoded_key);
+
+ std::stringstream encryption_header;
+ encryption_header << "rs=" << base::NumberToString(record_size) << ";";
+ encryption_header << "salt=" << encoded_salt;
+
+ message.data["encryption"] = encryption_header.str();
+ message.data["crypto-key"] = "dh=" + encoded_key;
+ message.raw_data.swap(ciphertext);
+ break;
+ }
+ case GCMMessageCryptographer::Version::DRAFT_08: {
+ ASSERT_NO_FATAL_FAILURE(
+ Encrypt(authorized_entity, public_key, auth_secret, kExampleMessage));
+ ASSERT_EQ(GCMEncryptionResult::ENCRYPTED_DRAFT_08, encryption_result());
+
+ message.data["content-encoding"] = "aes128gcm";
+ if (use_internal_raw_data_for_draft08) {
+ std::string raw_data_base64;
+ base::Base64Encode(encrypted_message(), &raw_data_base64);
+ message.data["_googRawData"] = raw_data_base64;
+ } else {
+ message.raw_data = encrypted_message();
+ }
+ break;
+ }
+ }
+
+ ASSERT_TRUE(encryption_provider()->IsEncryptedMessage(message));
+
+ // Decrypt the message, and expect everything to go wonderfully well.
+ ASSERT_NO_FATAL_FAILURE(Decrypt(message));
+ ASSERT_EQ(version == GCMMessageCryptographer::Version::DRAFT_03
+ ? GCMDecryptionResult::DECRYPTED_DRAFT_03
+ : GCMDecryptionResult::DECRYPTED_DRAFT_08,
+ decryption_result());
+
+ EXPECT_TRUE(decrypted_message().decrypted);
+ EXPECT_EQ(kExampleMessage, decrypted_message().raw_data);
+}
+
+void GCMEncryptionProviderTest::TestEncryptionNoKeys(
+ const std::string& app_id,
+ const std::string& authorized_entity) {
+ // Only create proper keys for receipeint without creating keys for sender.
+ ECPrivateKeyUniquePtr key;
+ std::string auth_secret;
+ encryption_provider()->key_store_->CreateKeys(
+ "receiver" + app_id, authorized_entity,
+ base::BindOnce(&GCMEncryptionProviderTest::HandleKeysCallback,
+ base::Unretained(this), &key, &auth_secret));
+
+ // Creating the public keys will be done asynchronously.
+ base::RunLoop().RunUntilIdle();
+
+ std::string public_key;
+ ASSERT_TRUE(GetRawPublicKey(*key, &public_key));
+ ASSERT_GT(public_key.size(), 0u);
+
+ ASSERT_NO_FATAL_FAILURE(
+ Encrypt(authorized_entity, public_key, auth_secret, kExampleMessage));
+ EXPECT_EQ(GCMEncryptionResult::NO_KEYS, encryption_result());
+}
+
+TEST_F(GCMEncryptionProviderTest, EncryptionRoundTripGCMRegistration) {
+ // GCMEncryptionProvider::DecryptMessage should succeed when the message was
+ // sent to a non-InstanceID GCM registration (empty authorized_entity).
+ ASSERT_NO_FATAL_FAILURE(TestEncryptionRoundTrip(
+ kExampleAppId, "" /* empty authorized entity for non-InstanceID */,
+ GCMMessageCryptographer::Version::DRAFT_03));
+}
+
+TEST_F(GCMEncryptionProviderTest, EncryptionRoundTripInstanceIDToken) {
+ // GCMEncryptionProvider::DecryptMessage should succeed when the message was
+ // sent to an InstanceID token (non-empty authorized_entity).
+ ASSERT_NO_FATAL_FAILURE(
+ TestEncryptionRoundTrip(kExampleAppId, kExampleAuthorizedEntity,
+ GCMMessageCryptographer::Version::DRAFT_03));
+}
+
+TEST_F(GCMEncryptionProviderTest, EncryptionRoundTripDraft08) {
+ // GCMEncryptionProvider::DecryptMessage should succeed when the message was
+ // encrypted following raft-ietf-webpush-encryption-08.
+ ASSERT_NO_FATAL_FAILURE(
+ TestEncryptionRoundTrip(kExampleAppId, kExampleAuthorizedEntity,
+ GCMMessageCryptographer::Version::DRAFT_08));
+}
+
+TEST_F(GCMEncryptionProviderTest, EncryptionRoundTripDraft08InternalRawData) {
+ // GCMEncryptionProvider::DecryptMessage should succeed when the message was
+ // encrypted following raft-ietf-webpush-encryption-08 with raw_data base64
+ // encoded in message data.
+ ASSERT_NO_FATAL_FAILURE(
+ TestEncryptionRoundTrip(kExampleAppId, kExampleAuthorizedEntity,
+ GCMMessageCryptographer::Version::DRAFT_08,
+ /*use_internal_raw_data_for_draft08=*/true));
+}
+
+TEST_F(GCMEncryptionProviderTest, EncryptionNoKeys) {
+ ASSERT_NO_FATAL_FAILURE(
+ TestEncryptionNoKeys(kExampleAppId, kExampleAuthorizedEntity));
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/crypto/gcm_encryption_result.h b/chromium/components/gcm_driver/crypto/gcm_encryption_result.h
new file mode 100644
index 00000000000..f06e4e56f42
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/gcm_encryption_result.h
@@ -0,0 +1,33 @@
+// Copyright 2019 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 COMPONENTS_GCM_DRIVER_CRYPTO_GCM_ENCRYPTION_RESULT_H_
+#define COMPONENTS_GCM_DRIVER_CRYPTO_GCM_ENCRYPTION_RESULT_H_
+
+namespace gcm {
+
+// Result of encrypting an outgoing message. The values of these reasons must
+// not be changed as they are being recorded using UMA. When adding a value,
+// please update GCMEncryptionResult in //tools/metrics/histograms/enums.xml.
+enum class GCMEncryptionResult {
+ // The message had been successfully be encrypted. The encryption scheme used
+ // for the message was draft-ietf-webpush-encryption-08.
+ ENCRYPTED_DRAFT_08 = 0,
+
+ // No public/private key-pair was associated with the app_id.
+ NO_KEYS = 1,
+
+ // The shared secret cannot be derived from the keying material.
+ INVALID_SHARED_SECRET = 2,
+
+ // The payload could not be encrypted as AES-128-GCM.
+ ENCRYPTION_FAILED = 3,
+
+ // Should be one more than the otherwise highest value in this enumeration.
+ ENUM_SIZE = ENCRYPTION_FAILED + 1
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_CRYPTO_GCM_ENCRYPTION_RESULT_H_
diff --git a/chromium/components/gcm_driver/crypto/gcm_key_store.cc b/chromium/components/gcm_driver/crypto/gcm_key_store.cc
new file mode 100644
index 00000000000..5a3f572ea12
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/gcm_key_store.cc
@@ -0,0 +1,425 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/crypto/gcm_key_store.h"
+
+#include <stddef.h>
+
+#include <utility>
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/callback_helpers.h"
+#include "base/logging.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/strings/string_util.h"
+#include "base/task/sequenced_task_runner.h"
+#include "components/gcm_driver/crypto/p256_key_util.h"
+#include "components/leveldb_proto/public/proto_database_provider.h"
+#include "components/leveldb_proto/public/shared_proto_database_client_list.h"
+#include "crypto/random.h"
+#include "third_party/leveldatabase/env_chromium.h"
+
+namespace gcm {
+
+namespace {
+
+using EntryVectorType =
+ leveldb_proto::ProtoDatabase<EncryptionData>::KeyEntryVector;
+
+// Number of cryptographically secure random bytes to generate as a key pair's
+// authentication secret. Must be at least 16 bytes.
+const size_t kAuthSecretBytes = 16;
+
+// Size cap for the leveldb log file before compression.
+const size_t kDatabaseWriteBufferSizeBytes = 16 * 1024;
+
+std::string DatabaseKey(const std::string& app_id,
+ const std::string& authorized_entity) {
+ DCHECK_EQ(std::string::npos, app_id.find(','));
+ DCHECK_EQ(std::string::npos, authorized_entity.find(','));
+ DCHECK_NE("*", authorized_entity) << "Wildcards require special handling";
+ return authorized_entity.empty()
+ ? app_id // No comma, for compatibility with existing keys.
+ : app_id + ',' + authorized_entity;
+}
+
+leveldb_env::Options CreateLevelDbOptions() {
+ leveldb_env::Options options;
+ options.create_if_missing = true;
+ options.max_open_files = 0; // Use minimum.
+ options.write_buffer_size = kDatabaseWriteBufferSizeBytes;
+ return options;
+}
+
+} // namespace
+
+enum class GCMKeyStore::State {
+ UNINITIALIZED,
+ INITIALIZING,
+ INITIALIZED,
+ FAILED
+};
+
+GCMKeyStore::GCMKeyStore(
+ const base::FilePath& key_store_path,
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner)
+ : key_store_path_(key_store_path),
+ blocking_task_runner_(blocking_task_runner),
+ state_(State::UNINITIALIZED) {
+ DCHECK(blocking_task_runner);
+}
+
+GCMKeyStore::~GCMKeyStore() {}
+
+void GCMKeyStore::GetKeys(const std::string& app_id,
+ const std::string& authorized_entity,
+ bool fallback_to_empty_authorized_entity,
+ KeysCallback callback) {
+ LazyInitialize(
+ base::BindOnce(&GCMKeyStore::GetKeysAfterInitialize,
+ weak_factory_.GetWeakPtr(), app_id, authorized_entity,
+ fallback_to_empty_authorized_entity, std::move(callback)));
+}
+
+void GCMKeyStore::GetKeysAfterInitialize(
+ const std::string& app_id,
+ const std::string& authorized_entity,
+ bool fallback_to_empty_authorized_entity,
+ KeysCallback callback) {
+ DCHECK(state_ == State::INITIALIZED || state_ == State::FAILED);
+ bool success = false;
+
+ if (state_ == State::INITIALIZED) {
+ auto outer_iter = key_data_.find(app_id);
+ if (outer_iter != key_data_.end()) {
+ const auto& inner_map = outer_iter->second;
+ auto inner_iter = inner_map.find(authorized_entity);
+ if (fallback_to_empty_authorized_entity && inner_iter == inner_map.end())
+ inner_iter = inner_map.find(std::string());
+ if (inner_iter != inner_map.end()) {
+ const auto& map_entry = inner_iter->second;
+ std::move(callback).Run(map_entry.first->Copy(), map_entry.second);
+ success = true;
+ }
+ }
+ }
+
+ UMA_HISTOGRAM_BOOLEAN("GCM.Crypto.GetKeySuccessRate", success);
+ if (!success)
+ std::move(callback).Run(nullptr /* key */, std::string() /* auth_secret */);
+}
+
+void GCMKeyStore::CreateKeys(const std::string& app_id,
+ const std::string& authorized_entity,
+ KeysCallback callback) {
+ LazyInitialize(base::BindOnce(&GCMKeyStore::CreateKeysAfterInitialize,
+ weak_factory_.GetWeakPtr(), app_id,
+ authorized_entity, std::move(callback)));
+}
+
+void GCMKeyStore::CreateKeysAfterInitialize(
+ const std::string& app_id,
+ const std::string& authorized_entity,
+ KeysCallback callback) {
+ DCHECK(state_ == State::INITIALIZED || state_ == State::FAILED);
+ if (state_ != State::INITIALIZED) {
+ std::move(callback).Run(nullptr /* key */, std::string() /* auth_secret */);
+ return;
+ }
+
+ // Only allow creating new keys if no keys currently exist. Multiple Instance
+ // ID tokens can share an app_id (with different authorized entities), but
+ // InstanceID tokens can't share an app_id with a non-InstanceID registration.
+ // This invariant is necessary for the fallback_to_empty_authorized_entity
+ // mode of GetKey (needed by GCMEncryptionProvider::DecryptMessage, which
+ // can't distinguish Instance ID tokens from non-InstanceID registrations).
+ DCHECK(!key_data_.count(app_id) ||
+ (!authorized_entity.empty() &&
+ !key_data_[app_id].count(authorized_entity) &&
+ !key_data_[app_id].count(std::string())))
+ << "Instance ID tokens cannot share an app_id with a non-InstanceID GCM "
+ "registration";
+
+ std::unique_ptr<crypto::ECPrivateKey> key(crypto::ECPrivateKey::Create());
+
+ if (!key) {
+ NOTREACHED() << "Unable to initialize a P-256 key pair.";
+
+ std::move(callback).Run(nullptr /* key */, std::string() /* auth_secret */);
+ return;
+ }
+
+ std::string auth_secret;
+
+ // Create the authentication secret, which has to be a cryptographically
+ // secure random number of at least 128 bits (16 bytes).
+ crypto::RandBytes(base::WriteInto(&auth_secret, kAuthSecretBytes + 1),
+ kAuthSecretBytes);
+
+ // Store the keys in a new EncryptionData object.
+ EncryptionData encryption_data;
+ encryption_data.set_app_id(app_id);
+ if (!authorized_entity.empty())
+ encryption_data.set_authorized_entity(authorized_entity);
+ encryption_data.set_auth_secret(auth_secret);
+
+ std::string private_key;
+ bool success = GetRawPrivateKey(*key, &private_key);
+ DCHECK(success);
+ encryption_data.set_private_key(private_key);
+
+ // Write them immediately to our cache, so subsequent calls to
+ // {Get/Create/Remove}Keys can see them.
+ key_data_[app_id][authorized_entity] = {key->Copy(), auth_secret};
+
+ std::unique_ptr<EntryVectorType> entries_to_save(new EntryVectorType());
+ std::unique_ptr<std::vector<std::string>> keys_to_remove(
+ new std::vector<std::string>());
+
+ entries_to_save->push_back(
+ std::make_pair(DatabaseKey(app_id, authorized_entity), encryption_data));
+
+ database_->UpdateEntries(
+ std::move(entries_to_save), std::move(keys_to_remove),
+ base::BindOnce(&GCMKeyStore::DidStoreKeys, weak_factory_.GetWeakPtr(),
+ std::move(key), auth_secret, std::move(callback)));
+}
+
+void GCMKeyStore::DidStoreKeys(std::unique_ptr<crypto::ECPrivateKey> pair,
+ const std::string& auth_secret,
+ KeysCallback callback,
+ bool success) {
+ UMA_HISTOGRAM_BOOLEAN("GCM.Crypto.CreateKeySuccessRate", success);
+
+ if (!success) {
+ DVLOG(1) << "Unable to store the created key in the GCM Key Store.";
+
+ // Our cache is now inconsistent. Reject requests until restarted.
+ state_ = State::FAILED;
+
+ std::move(callback).Run(nullptr /* key */, std::string() /* auth_secret */);
+ return;
+ }
+
+ std::move(callback).Run(std::move(pair), auth_secret);
+}
+
+void GCMKeyStore::RemoveKeys(const std::string& app_id,
+ const std::string& authorized_entity,
+ base::OnceClosure callback) {
+ LazyInitialize(base::BindOnce(&GCMKeyStore::RemoveKeysAfterInitialize,
+ weak_factory_.GetWeakPtr(), app_id,
+ authorized_entity, std::move(callback)));
+}
+
+void GCMKeyStore::RemoveKeysAfterInitialize(
+ const std::string& app_id,
+ const std::string& authorized_entity,
+ base::OnceClosure callback) {
+ DCHECK(state_ == State::INITIALIZED || state_ == State::FAILED);
+
+ const auto& outer_iter = key_data_.find(app_id);
+ if (outer_iter == key_data_.end() || state_ != State::INITIALIZED) {
+ std::move(callback).Run();
+ return;
+ }
+
+ std::unique_ptr<EntryVectorType> entries_to_save(new EntryVectorType());
+ std::unique_ptr<std::vector<std::string>> keys_to_remove(
+ new std::vector<std::string>());
+
+ bool had_keys = false;
+ auto& inner_map = outer_iter->second;
+ for (auto it = inner_map.begin(); it != inner_map.end();) {
+ // Wildcard "*" matches all non-empty authorized entities (InstanceID only).
+ if (authorized_entity == "*" ? !it->first.empty()
+ : it->first == authorized_entity) {
+ had_keys = true;
+
+ keys_to_remove->push_back(DatabaseKey(app_id, it->first));
+
+ // Clear keys immediately from our cache, so subsequent calls to
+ // {Get/Create/Remove}Keys don't see them.
+ it = inner_map.erase(it);
+ } else {
+ ++it;
+ }
+ }
+ if (!had_keys) {
+ std::move(callback).Run();
+ return;
+ }
+ if (inner_map.empty())
+ key_data_.erase(app_id);
+
+ database_->UpdateEntries(
+ std::move(entries_to_save), std::move(keys_to_remove),
+ base::BindOnce(&GCMKeyStore::DidRemoveKeys, weak_factory_.GetWeakPtr(),
+ std::move(callback)));
+}
+
+void GCMKeyStore::DidRemoveKeys(base::OnceClosure callback, bool success) {
+ UMA_HISTOGRAM_BOOLEAN("GCM.Crypto.RemoveKeySuccessRate", success);
+
+ if (!success) {
+ DVLOG(1) << "Unable to delete a key from the GCM Key Store.";
+
+ // Our cache is now inconsistent. Reject requests until restarted.
+ state_ = State::FAILED;
+ }
+
+ std::move(callback).Run();
+}
+
+void GCMKeyStore::DidUpgradeDatabase(bool success) {
+ UMA_HISTOGRAM_BOOLEAN("GCM.Crypto.GCMDatabaseUpgradeResult", success);
+ if (!success) {
+ DVLOG(1) << "Unable to upgrade the GCM Key Store database.";
+ // Our cache is now inconsistent. Reject requests until restarted.
+ state_ = State::FAILED;
+ delayed_task_controller_.SetReady();
+ return;
+ }
+
+ database_->LoadEntries(
+ base::BindOnce(&GCMKeyStore::DidLoadKeys, weak_factory_.GetWeakPtr()));
+}
+
+void GCMKeyStore::LazyInitialize(base::OnceClosure done_closure) {
+ if (delayed_task_controller_.CanRunTaskWithoutDelay()) {
+ std::move(done_closure).Run();
+ return;
+ }
+
+ delayed_task_controller_.AddTask(std::move(done_closure));
+ if (state_ == State::INITIALIZING)
+ return;
+
+ state_ = State::INITIALIZING;
+
+ database_ = leveldb_proto::ProtoDatabaseProvider::GetUniqueDB<EncryptionData>(
+ leveldb_proto::ProtoDbType::GCM_KEY_STORE, key_store_path_,
+ blocking_task_runner_);
+
+ database_->Init(
+ CreateLevelDbOptions(),
+ base::BindOnce(&GCMKeyStore::DidInitialize, weak_factory_.GetWeakPtr()));
+}
+
+void GCMKeyStore::DidInitialize(leveldb_proto::Enums::InitStatus status) {
+ bool success = status == leveldb_proto::Enums::kOK;
+ UMA_HISTOGRAM_BOOLEAN("GCM.Crypto.InitKeyStoreSuccessRate", success);
+ if (!success) {
+ DVLOG(1) << "Unable to initialize the GCM Key Store.";
+ state_ = State::FAILED;
+
+ delayed_task_controller_.SetReady();
+ return;
+ }
+
+ database_->LoadEntries(
+ base::BindOnce(&GCMKeyStore::DidLoadKeys, weak_factory_.GetWeakPtr()));
+}
+
+void GCMKeyStore::UpgradeDatabase(
+ std::unique_ptr<std::vector<EncryptionData>> entries) {
+ std::unique_ptr<EntryVectorType> entries_to_save =
+ std::make_unique<EntryVectorType>();
+ std::unique_ptr<std::vector<std::string>> keys_to_remove =
+ std::make_unique<std::vector<std::string>>();
+
+ // Loop over entries, create list of database entries to overwrite.
+ for (EncryptionData& entry : *entries) {
+ if (!entry.keys_size())
+ continue;
+ std::string decrypted_private_key;
+ if (!DecryptPrivateKey(entry.keys(0).private_key(),
+ &decrypted_private_key)) {
+ DVLOG(1) << "Unable to decrypt private key: "
+ << entry.keys(0).private_key();
+ state_ = State::FAILED;
+ delayed_task_controller_.SetReady();
+ UMA_HISTOGRAM_BOOLEAN("GCM.Crypto.GCMDatabaseUpgradeResult",
+ false /* sucess */);
+ return;
+ }
+
+ entry.set_private_key(decrypted_private_key);
+ entry.clear_keys();
+ entries_to_save->push_back(std::make_pair(
+ DatabaseKey(entry.app_id(), entry.authorized_entity()), entry));
+ }
+
+ database_->UpdateEntries(std::move(entries_to_save),
+ std::move(keys_to_remove),
+ base::BindOnce(&GCMKeyStore::DidUpgradeDatabase,
+ weak_factory_.GetWeakPtr()));
+}
+
+void GCMKeyStore::DidLoadKeys(
+ bool success,
+ std::unique_ptr<std::vector<EncryptionData>> entries) {
+ UMA_HISTOGRAM_BOOLEAN("GCM.Crypto.LoadKeyStoreSuccessRate", success);
+ if (!success) {
+ DVLOG(1) << "Unable to load entries into the GCM Key Store.";
+ state_ = State::FAILED;
+
+ delayed_task_controller_.SetReady();
+ return;
+ }
+
+ for (const EncryptionData& entry : *entries) {
+ std::string authorized_entity;
+ if (entry.has_authorized_entity())
+ authorized_entity = entry.authorized_entity();
+ std::unique_ptr<crypto::ECPrivateKey> key;
+
+ // The old format of EncryptionData has a KeyPair in it. Previously
+ // we used to cache the key pair and auth secret in key_data_.
+ // The new code adds the pair {ECPrivateKey, auth_secret} to
+ // key_data_ instead.
+ if (entry.keys_size()) {
+ if (state_ == State::FAILED)
+ return;
+
+ // Old format of EncryptionData. Upgrade database so there are no such
+ // entries. We'll reload keys from the database once this is done.
+ UpgradeDatabase(std::move(entries));
+ return;
+ } else {
+ std::string private_key_str = entry.private_key();
+ if (private_key_str.empty())
+ continue;
+ std::vector<uint8_t> private_key(private_key_str.begin(),
+ private_key_str.end());
+ key = crypto::ECPrivateKey::CreateFromPrivateKeyInfo(private_key);
+ }
+
+ key_data_[entry.app_id()][authorized_entity] =
+ std::make_pair(std::move(key), entry.auth_secret());
+ }
+
+ state_ = State::INITIALIZED;
+
+ delayed_task_controller_.SetReady();
+}
+
+bool GCMKeyStore::DecryptPrivateKey(const std::string& to_decrypt,
+ std::string* decrypted) {
+ DCHECK(decrypted);
+ std::vector<uint8_t> to_decrypt_vector(to_decrypt.begin(), to_decrypt.end());
+ std::unique_ptr<crypto::ECPrivateKey> key_to_decrypt =
+ crypto::ECPrivateKey::CreateFromEncryptedPrivateKeyInfo(
+ to_decrypt_vector);
+ if (!key_to_decrypt)
+ return false;
+ std::vector<uint8_t> decrypted_vector;
+ if (!key_to_decrypt->ExportPrivateKey(&decrypted_vector))
+ return false;
+ decrypted->assign(decrypted_vector.begin(), decrypted_vector.end());
+ return true;
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/crypto/gcm_key_store.h b/chromium/components/gcm_driver/crypto/gcm_key_store.h
new file mode 100644
index 00000000000..0c05923dc46
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/gcm_key_store.h
@@ -0,0 +1,150 @@
+// Copyright 2015 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 COMPONENTS_GCM_DRIVER_CRYPTO_GCM_KEY_STORE_H_
+#define COMPONENTS_GCM_DRIVER_CRYPTO_GCM_KEY_STORE_H_
+
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include "base/callback_forward.h"
+#include "base/files/file_path.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "components/gcm_driver/crypto/proto/gcm_encryption_data.pb.h"
+#include "components/gcm_driver/gcm_delayed_task_controller.h"
+#include "components/leveldb_proto/public/proto_database.h"
+#include "crypto/ec_private_key.h"
+
+namespace base {
+class SequencedTaskRunner;
+}
+
+namespace gcm {
+
+// Key storage for use with encrypted messages received from Google Cloud
+// Messaging. It provides the ability to create and store a key-pair for a given
+// app id + authorized entity pair, and to retrieve and delete key-pairs.
+//
+// This class is backed by a proto database and might end up doing file I/O on
+// a background task runner. For this reason, all public APIs take a callback
+// rather than returning the result. Do not rely on the timing of the callbacks.
+class GCMKeyStore {
+ public:
+ using KeysCallback =
+ base::OnceCallback<void(std::unique_ptr<crypto::ECPrivateKey> key,
+ const std::string& auth_secret)>;
+
+ GCMKeyStore(
+ const base::FilePath& key_store_path,
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner);
+
+ GCMKeyStore(const GCMKeyStore&) = delete;
+ GCMKeyStore& operator=(const GCMKeyStore&) = delete;
+
+ ~GCMKeyStore();
+
+ // Retrieves the public/private key-pair associated with the |app_id| +
+ // |authorized_entity| pair, and invokes |callback| when they are available,
+ // or when an error occurred. |authorized_entity| should be the InstanceID
+ // token's authorized entity, or "" for non-InstanceID GCM registrations. If
+ // |fallback_to_empty_authorized_entity| is true and the keys are not found,
+ // GetKeys will try again with an empty authorized entity; this can be used
+ // when it's not known whether or not the |app_id| is for an InstanceID.
+ void GetKeys(const std::string& app_id,
+ const std::string& authorized_entity,
+ bool fallback_to_empty_authorized_entity,
+ KeysCallback callback);
+
+ // Creates a new public/private key-pair for the |app_id| +
+ // |authorized_entity| pair, and invokes |callback| when they are available,
+ // or when an error occurred. |authorized_entity| should be the InstanceID
+ // token's authorized entity, or "" for non-InstanceID GCM registrations.
+ // Simultaneously using the same |app_id| for both a non-InstanceID GCM
+ // registration and one or more InstanceID tokens is not supported.
+ void CreateKeys(const std::string& app_id,
+ const std::string& authorized_entity,
+ KeysCallback callback);
+
+ // Removes the keys associated with the |app_id| + |authorized_entity| pair,
+ // and invokes |callback| when the operation has finished. |authorized_entity|
+ // should be the InstanceID token's authorized entity, or "*" to remove for
+ // all InstanceID tokens, or "" for non-InstanceID GCM registrations.
+ void RemoveKeys(const std::string& app_id,
+ const std::string& authorized_entity,
+ base::OnceClosure callback);
+
+ private:
+ friend class GCMKeyStoreTest;
+ // Initializes the database if necessary, and runs |done_closure| when done.
+ void LazyInitialize(base::OnceClosure done_closure);
+
+ // Upgrades the stored encryption keys from pairs including deprecated PKCS #8
+ // EncryptedPrivateKeyInfo blocks, to storing a single PrivateKeyInfo block.
+ void UpgradeDatabase(std::unique_ptr<std::vector<EncryptionData>> entries);
+
+ void DidInitialize(leveldb_proto::Enums::InitStatus status);
+ void DidLoadKeys(bool success,
+ std::unique_ptr<std::vector<EncryptionData>> entries);
+ void DidStoreKeys(std::unique_ptr<crypto::ECPrivateKey> key,
+ const std::string& auth_secret,
+ KeysCallback callback,
+ bool success);
+ void DidUpgradeDatabase(bool success);
+
+ void DidRemoveKeys(base::OnceClosure callback, bool success);
+
+ // Private implementations of the API that will be executed when the database
+ // has either been successfully loaded, or failed to load.
+
+ void GetKeysAfterInitialize(const std::string& app_id,
+ const std::string& authorized_entity,
+ bool fallback_to_empty_authorized_entity,
+ KeysCallback callback);
+ void CreateKeysAfterInitialize(const std::string& app_id,
+ const std::string& authorized_entity,
+ KeysCallback callback);
+ void RemoveKeysAfterInitialize(const std::string& app_id,
+ const std::string& authorized_entity,
+ base::OnceClosure callback);
+
+ // Converts private key from old deprecated format (where it is encrypted with
+ // and empty string) to the new format, where it's unencrypted.
+ bool DecryptPrivateKey(const std::string& to_decrypt, std::string* decrypted);
+
+ // Path in which the key store database will be saved.
+ base::FilePath key_store_path_;
+
+ // Blocking task runner which the database will do I/O operations on.
+ scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_;
+
+ // Instance of the ProtoDatabase backing the key store.
+ std::unique_ptr<leveldb_proto::ProtoDatabase<EncryptionData>> database_;
+
+ enum class State;
+
+ // The current state of the database. It has to be initialized before use.
+ State state_;
+
+ // Controller for tasks that should be executed once the key store has
+ // finished initializing.
+ GCMDelayedTaskController delayed_task_controller_;
+
+ // Nested map from app_id to a map from authorized_entity to the loaded key
+ // pair and authentication secrets.
+ using KeyPairAndAuthSecret =
+ std::pair<std::unique_ptr<crypto::ECPrivateKey>, std::string>;
+ std::unordered_map<std::string,
+ std::unordered_map<std::string, KeyPairAndAuthSecret>>
+ key_data_;
+
+ base::WeakPtrFactory<GCMKeyStore> weak_factory_{this};
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_CRYPTO_GCM_KEY_STORE_H_
diff --git a/chromium/components/gcm_driver/crypto/gcm_key_store_unittest.cc b/chromium/components/gcm_driver/crypto/gcm_key_store_unittest.cc
new file mode 100644
index 00000000000..a2b98121982
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/gcm_key_store_unittest.cc
@@ -0,0 +1,775 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/crypto/gcm_key_store.h"
+
+#include <memory>
+#include <string>
+
+#include "base/base64url.h"
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/check_op.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/run_loop.h"
+#include "base/strings/string_util.h"
+#include "base/test/gtest_util.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/task_environment.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "components/gcm_driver/crypto/p256_key_util.h"
+#include "components/leveldb_proto/public/proto_database.h"
+#include "crypto/random.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+
+using ECPrivateKeyUniquePtr = std::unique_ptr<crypto::ECPrivateKey>;
+using EncryptDataVectorUniquePtr = std::unique_ptr<std::vector<EncryptionData>>;
+using EntryVectorType =
+ leveldb_proto::ProtoDatabase<EncryptionData>::KeyEntryVector;
+
+const char kFakeAppId[] = "my_app_id";
+const char kSecondFakeAppId[] = "my_other_app_id";
+const char kFakeAuthorizedEntity[] = "my_sender_id";
+const char kSecondFakeAuthorizedEntity[] = "my_other_sender_id";
+const char kPrivateEncrypted[] =
+ "MIGxMBwGCiqGSIb3DQEMAQMwDgQIh9aZ3UvuDloCAggABIGQZ-T8CJZe-no4mOTDgX1Gm986"
+ "Gsbe3mjJeABhA4KOmut_qJh5kt_DLqdNShiQr-afk3AdkX-fxLZdrcHiW9aWvBjnMAY65zg5"
+ "oHsuUaoEuG88Ksbku2u193OENWTQTsYaYE2O44qmRfsX773UNVcWXg_omwIbhbgf6tLZUZH_"
+ "dTC3YjzuxjbSP89HPEJ-eBXA";
+const char kPrivateDecrypted[] =
+ "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgnCScek-QpEjmOOlT-rQ38nZz"
+ "vdPlqa00Zy0i6m2OJvahRANCAATaEQ22_OCRpvIOWeQhcbq0qrF1iddSLX1xFmFSxPOWOwmJ"
+ "A417CBHOGqsWGkNRvAapFwiegz6Q61rXVo_5roB1";
+const char kPublicKey[] =
+ "BNoRDbb84JGm8g5Z5CFxurSqsXWJ11ItfXEWYVLE85Y7CYkDjXsIEc4aqxYaQ1G8BqkXCJ6D"
+ "PpDrWtdWj_mugHU";
+
+// Number of cryptographically secure random bytes to generate as a key pair's
+// authentication secret. Must be at least 16 bytes.
+const size_t kAuthSecretBytes = 16;
+
+} // namespace
+
+class GCMKeyStoreTest : public ::testing::Test {
+ public:
+ GCMKeyStoreTest() {}
+ ~GCMKeyStoreTest() override {}
+
+ void SetUp() override {
+ ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
+ CreateKeyStore();
+ }
+
+ void TearDown() override {
+ gcm_key_store_.reset();
+
+ // |gcm_key_store_| owns a ProtoDatabase whose destructor deletes the
+ // underlying LevelDB database on the task runner.
+ base::RunLoop().RunUntilIdle();
+ }
+
+ // Creates the GCM Key Store instance. May be called from within a test's body
+ // to re-create the key store, causing the database to re-open.
+ void CreateKeyStore() {
+ gcm_key_store_ = std::make_unique<GCMKeyStore>(
+ scoped_temp_dir_.GetPath(),
+ task_environment_.GetMainThreadTaskRunner());
+ }
+
+ // Callback to use with GCMKeyStore::{GetKeys, CreateKeys} calls.
+ void GotKeys(ECPrivateKeyUniquePtr* key_out,
+ std::string* auth_secret_out,
+ base::OnceClosure quit_closure,
+ ECPrivateKeyUniquePtr key,
+ const std::string& auth_secret) {
+ *key_out = std::move(key);
+ *auth_secret_out = auth_secret;
+ if (quit_closure)
+ std::move(quit_closure).Run();
+ }
+
+ void AddOldFormatEncryptionDataToKeyStoreDatabase(
+ const std::string& app_id,
+ const std::string& authorized_entity) {
+ EncryptionData encryption_data;
+ encryption_data.set_app_id(app_id);
+ encryption_data.set_authorized_entity(authorized_entity);
+
+ // Create the authentication secret, which has to be a cryptographically
+ // secure random number of at least 128 bits (16 bytes).
+ std::string auth_secret;
+ crypto::RandBytes(base::WriteInto(&auth_secret, kAuthSecretBytes + 1),
+ kAuthSecretBytes);
+ encryption_data.set_auth_secret(auth_secret);
+
+ // Add keys.
+ KeyPair* pair = encryption_data.add_keys();
+ pair->set_type(KeyPair::ECDH_P256);
+ std::string private_key;
+ ASSERT_TRUE(base::Base64UrlDecode(
+ kPrivateEncrypted, base::Base64UrlDecodePolicy::IGNORE_PADDING,
+ &private_key));
+ pair->set_private_key(private_key);
+ std::string public_key;
+ ASSERT_TRUE(base::Base64UrlDecode(
+ kPublicKey, base::Base64UrlDecodePolicy::IGNORE_PADDING, &public_key));
+ pair->set_public_key(public_key);
+
+ // Add this to database.
+ std::unique_ptr<EntryVectorType> entries_to_save =
+ std::make_unique<EntryVectorType>();
+ std::unique_ptr<std::vector<std::string>> keys_to_remove =
+ std::make_unique<std::vector<std::string>>();
+ entries_to_save->push_back(std::make_pair(
+ encryption_data.app_id() + ',' + encryption_data.authorized_entity(),
+ encryption_data));
+ base::RunLoop run_loop;
+ gcm_key_store_->database_->UpdateEntries(
+ std::move(entries_to_save), std::move(keys_to_remove),
+ base::BindOnce(&GCMKeyStoreTest::UpdatedEntries, base::Unretained(this),
+ run_loop.QuitClosure()));
+ run_loop.Run();
+ }
+
+ protected:
+ GCMKeyStore* gcm_key_store() { return gcm_key_store_.get(); }
+ base::HistogramTester* histogram_tester() { return &histogram_tester_; }
+
+ void UpdatedEntries(base::OnceClosure quit_closure, bool success) {
+ EXPECT_TRUE(success);
+ if (quit_closure)
+ std::move(quit_closure).Run();
+ }
+
+ private:
+ base::test::SingleThreadTaskEnvironment task_environment_;
+ base::ScopedTempDir scoped_temp_dir_;
+ base::HistogramTester histogram_tester_;
+
+ std::unique_ptr<GCMKeyStore> gcm_key_store_;
+};
+
+TEST_F(GCMKeyStoreTest, EmptyByDefault) {
+ // The key store is initialized lazily, so this histogram confirms that
+ // calling the constructor does not in fact cause initialization.
+ histogram_tester()->ExpectTotalCount(
+ "GCM.Crypto.InitKeyStoreSuccessRate", 0);
+
+ ECPrivateKeyUniquePtr key;
+ std::string auth_secret;
+ base::RunLoop run_loop;
+ gcm_key_store()->GetKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key,
+ &auth_secret, run_loop.QuitClosure()));
+
+ run_loop.Run();
+
+ ASSERT_FALSE(key);
+ EXPECT_EQ(0u, auth_secret.size());
+
+ histogram_tester()->ExpectBucketCount(
+ "GCM.Crypto.GetKeySuccessRate", 0, 1); // failure
+}
+
+TEST_F(GCMKeyStoreTest, CreateAndGetKeys) {
+ ECPrivateKeyUniquePtr key;
+ std::string auth_secret;
+ base::RunLoop run_loop;
+ gcm_key_store()->CreateKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key,
+ &auth_secret, run_loop.QuitClosure()));
+
+ run_loop.Run();
+
+ ASSERT_TRUE(key);
+ std::string public_key, private_key;
+ ASSERT_TRUE(GetRawPrivateKey(*key, &private_key));
+ ASSERT_TRUE(GetRawPublicKey(*key, &public_key));
+
+ EXPECT_GT(public_key.size(), 0u);
+ EXPECT_GT(private_key.size(), 0u);
+
+ ASSERT_GT(auth_secret.size(), 0u);
+ histogram_tester()->ExpectBucketCount(
+ "GCM.Crypto.CreateKeySuccessRate", 1, 1); // success
+
+ ECPrivateKeyUniquePtr read_key;
+ std::string read_auth_secret;
+ base::RunLoop first_get_run_loop;
+ gcm_key_store()->GetKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &read_key, &read_auth_secret,
+ first_get_run_loop.QuitClosure()));
+
+ first_get_run_loop.Run();
+
+ ASSERT_TRUE(read_key);
+ std::string read_public_key, read_private_key;
+ ASSERT_TRUE(GetRawPrivateKey(*read_key, &read_private_key));
+ ASSERT_TRUE(GetRawPublicKey(*read_key, &read_public_key));
+ ASSERT_EQ(read_private_key, private_key);
+ ASSERT_EQ(read_public_key, public_key);
+ EXPECT_EQ(auth_secret, read_auth_secret);
+
+ histogram_tester()->ExpectBucketCount("GCM.Crypto.GetKeySuccessRate", 1,
+ 1); // success
+
+ // GetKey should also succeed if fallback_to_empty_authorized_entity is true
+ // (fallback should not occur, since an exact match is found).
+ base::RunLoop second_get_run_loop;
+ gcm_key_store()->GetKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ true /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &read_key, &read_auth_secret,
+ second_get_run_loop.QuitClosure()));
+
+ second_get_run_loop.Run();
+
+ ASSERT_TRUE(read_key);
+
+ ASSERT_TRUE(GetRawPrivateKey(*read_key, &read_private_key));
+ ASSERT_TRUE(GetRawPublicKey(*read_key, &read_public_key));
+ ASSERT_EQ(read_private_key, private_key);
+ ASSERT_EQ(read_public_key, public_key);
+ EXPECT_EQ(auth_secret, read_auth_secret);
+
+ histogram_tester()->ExpectBucketCount("GCM.Crypto.GetKeySuccessRate", 1,
+ 2); // another success
+}
+
+TEST_F(GCMKeyStoreTest, GetKeysFallback) {
+ ECPrivateKeyUniquePtr key;
+ std::string auth_secret;
+ {
+ base::RunLoop run_loop;
+ gcm_key_store()->CreateKeys(
+ kFakeAppId, "" /* empty authorized entity for non-InstanceID */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key,
+ &auth_secret, run_loop.QuitClosure()));
+
+ run_loop.Run();
+ }
+
+ ASSERT_TRUE(key);
+
+ std::string public_key, private_key;
+ ASSERT_TRUE(GetRawPrivateKey(*key, &private_key));
+ ASSERT_TRUE(GetRawPublicKey(*key, &public_key));
+
+ EXPECT_GT(public_key.size(), 0u);
+ EXPECT_GT(private_key.size(), 0u);
+ ASSERT_GT(auth_secret.size(), 0u);
+
+ histogram_tester()->ExpectBucketCount("GCM.Crypto.CreateKeySuccessRate", 1,
+ 1); // success
+
+ // GetKeys should fail when fallback_to_empty_authorized_entity is false, as
+ // there is not an exact match for kFakeAuthorizedEntity.
+ ECPrivateKeyUniquePtr read_key;
+ std::string read_auth_secret;
+ {
+ base::RunLoop run_loop;
+ gcm_key_store()->GetKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &read_key, &read_auth_secret, run_loop.QuitClosure()));
+
+ run_loop.Run();
+ }
+
+ ASSERT_FALSE(read_key);
+ EXPECT_EQ(0u, read_auth_secret.size());
+
+ histogram_tester()->ExpectBucketCount("GCM.Crypto.GetKeySuccessRate", 0,
+ 1); // failure
+
+ // GetKey should succeed when fallback_to_empty_authorized_entity is true, as
+ // falling back to empty authorized entity will match the created key.
+ {
+ base::RunLoop run_loop;
+ gcm_key_store()->GetKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ true /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &read_key, &read_auth_secret, run_loop.QuitClosure()));
+
+ run_loop.Run();
+ }
+
+ ASSERT_TRUE(read_key);
+
+ std::string read_public_key, read_private_key;
+ ASSERT_TRUE(GetRawPrivateKey(*key, &read_private_key));
+ ASSERT_TRUE(GetRawPublicKey(*key, &read_public_key));
+ EXPECT_EQ(private_key, read_private_key);
+ EXPECT_EQ(public_key, read_public_key);
+
+ EXPECT_EQ(auth_secret, read_auth_secret);
+
+ histogram_tester()->ExpectBucketCount("GCM.Crypto.GetKeySuccessRate", 1,
+ 1); // success
+}
+
+TEST_F(GCMKeyStoreTest, KeysPersistenceBetweenInstances) {
+ ECPrivateKeyUniquePtr key;
+ std::string auth_secret;
+ {
+ base::RunLoop run_loop;
+ gcm_key_store()->CreateKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key,
+ &auth_secret, run_loop.QuitClosure()));
+
+ run_loop.Run();
+ }
+
+ ASSERT_TRUE(key);
+
+ histogram_tester()->ExpectBucketCount(
+ "GCM.Crypto.InitKeyStoreSuccessRate", 1, 1); // success
+ histogram_tester()->ExpectBucketCount(
+ "GCM.Crypto.LoadKeyStoreSuccessRate", 1, 1); // success
+
+ // Create a new GCM Key Store instance.
+ CreateKeyStore();
+
+ ECPrivateKeyUniquePtr read_key;
+ std::string read_auth_secret;
+ {
+ base::RunLoop run_loop;
+ gcm_key_store()->GetKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &read_key, &read_auth_secret, run_loop.QuitClosure()));
+
+ run_loop.Run();
+ }
+
+ ASSERT_TRUE(read_key);
+ EXPECT_GT(read_auth_secret.size(), 0u);
+
+ histogram_tester()->ExpectBucketCount(
+ "GCM.Crypto.InitKeyStoreSuccessRate", 1, 2); // success
+ histogram_tester()->ExpectBucketCount(
+ "GCM.Crypto.LoadKeyStoreSuccessRate", 1, 2); // success
+}
+
+TEST_F(GCMKeyStoreTest, CreateAndRemoveKeys) {
+ ECPrivateKeyUniquePtr key;
+ std::string auth_secret;
+ {
+ base::RunLoop run_loop;
+ gcm_key_store()->CreateKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key,
+ &auth_secret, run_loop.QuitClosure()));
+
+ run_loop.Run();
+ }
+
+ ASSERT_TRUE(key);
+
+ ECPrivateKeyUniquePtr read_key;
+ std::string read_auth_secret;
+ {
+ base::RunLoop run_loop;
+ gcm_key_store()->GetKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &read_key, &read_auth_secret, run_loop.QuitClosure()));
+
+ run_loop.Run();
+ }
+
+ ASSERT_TRUE(read_key);
+
+ gcm_key_store()->RemoveKeys(kFakeAppId, kFakeAuthorizedEntity,
+ base::DoNothing());
+
+ base::RunLoop().RunUntilIdle();
+
+ histogram_tester()->ExpectBucketCount(
+ "GCM.Crypto.RemoveKeySuccessRate", 1, 1); // success
+
+ {
+ base::RunLoop run_loop;
+ gcm_key_store()->GetKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &read_key, &read_auth_secret, run_loop.QuitClosure()));
+
+ run_loop.Run();
+ }
+
+ ASSERT_FALSE(read_key);
+}
+
+TEST_F(GCMKeyStoreTest, CreateGetAndRemoveKeysSynchronously) {
+ ECPrivateKeyUniquePtr key;
+ std::string auth_secret;
+ gcm_key_store()->CreateKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key,
+ &auth_secret, base::OnceClosure()));
+
+ // Continue synchronously, without running RunUntilIdle first.
+ ECPrivateKeyUniquePtr key_after_create;
+ std::string auth_secret_after_create;
+ gcm_key_store()->GetKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &key_after_create, &auth_secret_after_create,
+ base::OnceClosure()));
+
+ // Continue synchronously, without running RunUntilIdle first.
+ gcm_key_store()->RemoveKeys(kFakeAppId, kFakeAuthorizedEntity,
+ base::DoNothing());
+
+ // Continue synchronously, without running RunUntilIdle first.
+ ECPrivateKeyUniquePtr key_after_remove;
+ std::string auth_secret_after_remove;
+ gcm_key_store()->GetKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &key_after_remove, &auth_secret_after_remove,
+ base::OnceClosure()));
+
+ base::RunLoop().RunUntilIdle();
+
+ histogram_tester()->ExpectBucketCount("GCM.Crypto.RemoveKeySuccessRate", 1,
+ 1); // success
+
+ ECPrivateKeyUniquePtr key_after_idle;
+ std::string auth_secret_after_idle;
+ gcm_key_store()->GetKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &key_after_idle, &auth_secret_after_idle,
+ base::OnceClosure()));
+
+ base::RunLoop().RunUntilIdle();
+
+ ASSERT_TRUE(key);
+ ASSERT_TRUE(key_after_create);
+ EXPECT_FALSE(key_after_remove);
+ EXPECT_FALSE(key_after_idle);
+
+ std::string public_key, public_key_after_create;
+ ASSERT_TRUE(GetRawPublicKey(*key, &public_key));
+ ASSERT_TRUE(GetRawPublicKey(*key, &public_key_after_create));
+ EXPECT_EQ(public_key, public_key_after_create);
+
+ EXPECT_GT(auth_secret.size(), 0u);
+ EXPECT_EQ(auth_secret, auth_secret_after_create);
+ EXPECT_EQ("", auth_secret_after_remove);
+ EXPECT_EQ("", auth_secret_after_idle);
+}
+
+TEST_F(GCMKeyStoreTest, RemoveKeysWildcardAuthorizedEntity) {
+ ECPrivateKeyUniquePtr key1, key2, key3;
+ std::string auth_secret1, auth_secret2, auth_secret3;
+ gcm_key_store()->CreateKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key1,
+ &auth_secret1, base::OnceClosure()));
+ gcm_key_store()->CreateKeys(
+ kFakeAppId, kSecondFakeAuthorizedEntity,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key2,
+ &auth_secret2, base::OnceClosure()));
+ gcm_key_store()->CreateKeys(
+ kSecondFakeAppId, kFakeAuthorizedEntity,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key3,
+ &auth_secret3, base::OnceClosure()));
+
+ base::RunLoop().RunUntilIdle();
+
+ ASSERT_TRUE(key1);
+ ASSERT_TRUE(key2);
+ ASSERT_TRUE(key3);
+
+ ECPrivateKeyUniquePtr read_key1, read_key2, read_key3;
+ std::string read_auth_secret1, read_auth_secret2, read_auth_secret3;
+ gcm_key_store()->GetKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &read_key1, &read_auth_secret1, base::OnceClosure()));
+ gcm_key_store()->GetKeys(
+ kFakeAppId, kSecondFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &read_key2, &read_auth_secret2, base::OnceClosure()));
+ gcm_key_store()->GetKeys(
+ kSecondFakeAppId, kFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &read_key3, &read_auth_secret3, base::OnceClosure()));
+
+ base::RunLoop().RunUntilIdle();
+
+ ASSERT_TRUE(read_key1);
+ ASSERT_TRUE(read_key2);
+ ASSERT_TRUE(read_key3);
+
+ gcm_key_store()->RemoveKeys(kFakeAppId, "*" /* authorized_entity */,
+ base::DoNothing());
+
+ base::RunLoop().RunUntilIdle();
+
+ histogram_tester()->ExpectBucketCount("GCM.Crypto.RemoveKeySuccessRate", 1,
+ 1); // success
+
+ gcm_key_store()->GetKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &read_key1, &read_auth_secret1, base::OnceClosure()));
+ gcm_key_store()->GetKeys(
+ kFakeAppId, kSecondFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &read_key2, &read_auth_secret2, base::OnceClosure()));
+ gcm_key_store()->GetKeys(
+ kSecondFakeAppId, kFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &read_key3, &read_auth_secret3, base::OnceClosure()));
+
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_FALSE(read_key1);
+ EXPECT_FALSE(read_key2);
+ ASSERT_TRUE(read_key3);
+}
+
+TEST_F(GCMKeyStoreTest, GetKeysMultipleAppIds) {
+ ECPrivateKeyUniquePtr key;
+ std::string auth_secret;
+ {
+ base::RunLoop run_loop;
+ gcm_key_store()->CreateKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key,
+ &auth_secret, run_loop.QuitClosure()));
+
+ run_loop.Run();
+ }
+
+ ASSERT_TRUE(key);
+
+ {
+ base::RunLoop run_loop;
+ gcm_key_store()->CreateKeys(
+ kSecondFakeAppId, kSecondFakeAuthorizedEntity,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key,
+ &auth_secret, run_loop.QuitClosure()));
+
+ run_loop.Run();
+ }
+
+ ASSERT_TRUE(key);
+
+ ECPrivateKeyUniquePtr read_key;
+ std::string read_auth_secret;
+ {
+ base::RunLoop run_loop;
+ gcm_key_store()->GetKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &read_key, &read_auth_secret, run_loop.QuitClosure()));
+
+ run_loop.Run();
+ }
+
+ ASSERT_TRUE(read_key);
+}
+
+TEST_F(GCMKeyStoreTest, SuccessiveCallsBeforeInitialization) {
+ ECPrivateKeyUniquePtr key;
+ std::string auth_secret;
+ gcm_key_store()->CreateKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key,
+ &auth_secret, base::OnceClosure()));
+
+ // Deliberately do not run the message loop, so that the callback has not
+ // been resolved yet. The following EXPECT() ensures this.
+ EXPECT_FALSE(key);
+
+ ECPrivateKeyUniquePtr read_key;
+ std::string read_auth_secret;
+ gcm_key_store()->GetKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &read_key, &read_auth_secret, base::OnceClosure()));
+
+ EXPECT_FALSE(read_key);
+
+ // Now run the message loop. Both tasks should have finished executing. Due
+ // to the asynchronous nature of operations, however, we can't rely on the
+ // write to have finished before the read begins.
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_TRUE(key);
+}
+
+TEST_F(GCMKeyStoreTest, CannotShareAppIdFromGCMToInstanceID) {
+ ECPrivateKeyUniquePtr key_unused;
+ std::string auth_secret_unused;
+ {
+ base::RunLoop run_loop;
+ gcm_key_store()->CreateKeys(
+ kFakeAppId, "" /* empty authorized entity for non-InstanceID */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &key_unused, &auth_secret_unused,
+ run_loop.QuitClosure()));
+
+ run_loop.Run();
+ }
+
+ EXPECT_DCHECK_DEATH({
+ base::RunLoop run_loop;
+ gcm_key_store()->CreateKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &key_unused, &auth_secret_unused,
+ run_loop.QuitClosure()));
+
+ run_loop.Run();
+ });
+}
+
+TEST_F(GCMKeyStoreTest, CannotShareAppIdFromInstanceIDToGCM) {
+ ECPrivateKeyUniquePtr key_unused;
+ std::string auth_secret_unused;
+ {
+ base::RunLoop run_loop;
+ gcm_key_store()->CreateKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &key_unused, &auth_secret_unused,
+ run_loop.QuitClosure()));
+
+ run_loop.Run();
+ }
+
+ {
+ base::RunLoop run_loop;
+ gcm_key_store()->CreateKeys(
+ kFakeAppId, kSecondFakeAuthorizedEntity,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &key_unused, &auth_secret_unused,
+ run_loop.QuitClosure()));
+
+ run_loop.Run();
+ }
+
+ EXPECT_DCHECK_DEATH({
+ base::RunLoop run_loop;
+ gcm_key_store()->CreateKeys(
+ kFakeAppId, "" /* empty authorized entity for non-InstanceID */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this),
+ &key_unused, &auth_secret_unused,
+ run_loop.QuitClosure()));
+
+ run_loop.Run();
+ });
+}
+
+TEST_F(GCMKeyStoreTest, TestUpgradePathForKeyStorageDeprecation) {
+ // Expect Upgrade count to be 0.
+ histogram_tester()->ExpectTotalCount("GCM.Crypto.GCMDatabaseUpgradeResult",
+ 0);
+ // Initialize GCM store and the underlying levelDB database by trying
+ // to fetch keys.
+ ECPrivateKeyUniquePtr key;
+ std::string auth_secret;
+ {
+ base::RunLoop run_loop;
+ gcm_key_store()->GetKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key,
+ &auth_secret, run_loop.QuitClosure()));
+
+ run_loop.Run();
+ }
+ ASSERT_FALSE(key);
+ histogram_tester()->ExpectTotalCount("GCM.Crypto.GCMDatabaseUpgradeResult",
+ 0);
+
+ // Add old format Encryption Data.
+ ASSERT_NO_FATAL_FAILURE(AddOldFormatEncryptionDataToKeyStoreDatabase(
+ kFakeAppId, kFakeAuthorizedEntity));
+
+ // Create a new GCM Key Store instance, so we can initialize again.
+ CreateKeyStore();
+
+ // GetKeys again, verify private key is decrypted and we have upgraded
+ // database exactly once
+ {
+ base::RunLoop run_loop;
+ gcm_key_store()->GetKeys(
+ kFakeAppId, kFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key,
+ &auth_secret, run_loop.QuitClosure()));
+ run_loop.Run();
+ }
+
+ histogram_tester()->ExpectBucketCount("GCM.Crypto.GCMDatabaseUpgradeResult",
+ 1, 1);
+ ASSERT_TRUE(key);
+ ASSERT_GT(auth_secret.size(), 0u);
+
+ // Verify also that the private key is decrypted.
+ std::string read_private_key;
+ ASSERT_TRUE(GetRawPrivateKey(*key, &read_private_key));
+ std::string decrypted_private_key;
+ ASSERT_TRUE(base::Base64UrlDecode(kPrivateDecrypted,
+ base::Base64UrlDecodePolicy::IGNORE_PADDING,
+ &decrypted_private_key));
+ ASSERT_EQ(decrypted_private_key, read_private_key);
+
+ // AddOldFormatEncryptionDataToKeyStoreDatabase() again, different keys
+ ASSERT_NO_FATAL_FAILURE(AddOldFormatEncryptionDataToKeyStoreDatabase(
+ kSecondFakeAppId, kSecondFakeAuthorizedEntity));
+
+ // GetKeys on this one, should return nullptr
+ {
+ base::RunLoop run_loop;
+ gcm_key_store()->GetKeys(
+ kSecondFakeAppId, kSecondFakeAuthorizedEntity,
+ false /* fallback_to_empty_authorized_entity */,
+ base::BindOnce(&GCMKeyStoreTest::GotKeys, base::Unretained(this), &key,
+ &auth_secret, run_loop.QuitClosure()));
+ run_loop.Run();
+ }
+ ASSERT_FALSE(key);
+ ASSERT_EQ(auth_secret.size(), 0u);
+ // GCMDatabaseUpgradeResult should not have increased.
+ histogram_tester()->ExpectBucketCount("GCM.Crypto.GCMDatabaseUpgradeResult",
+ 1, 1);
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/crypto/gcm_message_cryptographer.cc b/chromium/components/gcm_driver/crypto/gcm_message_cryptographer.cc
new file mode 100644
index 00000000000..73256da1a8d
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/gcm_message_cryptographer.cc
@@ -0,0 +1,477 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/crypto/gcm_message_cryptographer.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <algorithm>
+#include <sstream>
+
+#include "base/logging.h"
+#include "base/notreached.h"
+#include "base/numerics/ostream_operators.h"
+#include "base/numerics/safe_math.h"
+#include "base/strings/strcat.h"
+#include "base/strings/string_util.h"
+#include "base/sys_byteorder.h"
+#include "crypto/hkdf.h"
+#include "third_party/boringssl/src/include/openssl/aead.h"
+
+namespace gcm {
+
+namespace {
+
+// Size, in bytes, of the nonce for a record. This must be at least the size
+// of a uint64_t, which is used to indicate the record sequence number.
+const uint64_t kNonceSize = 12;
+
+// The default record size as defined by httpbis-encryption-encoding-06.
+const size_t kDefaultRecordSize = 4096;
+
+// Key size, in bytes, of a valid AEAD_AES_128_GCM key.
+const size_t kContentEncryptionKeySize = 16;
+
+// The BoringSSL functions used to seal (encrypt) and open (decrypt) a payload
+// follow the same prototype, declared as follows.
+using EVP_AEAD_CTX_TransformFunction =
+ int(const EVP_AEAD_CTX *ctx, uint8_t *out, size_t *out_len,
+ size_t max_out_len, const uint8_t *nonce, size_t nonce_len,
+ const uint8_t *in, size_t in_len, const uint8_t *ad, size_t ad_len);
+
+// Implementation of draft 03 of the Web Push Encryption standard:
+// https://tools.ietf.org/html/draft-ietf-webpush-encryption-03
+// https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-02
+class WebPushEncryptionDraft03
+ : public GCMMessageCryptographer::EncryptionScheme {
+ public:
+ WebPushEncryptionDraft03() = default;
+
+ WebPushEncryptionDraft03(const WebPushEncryptionDraft03&) = delete;
+ WebPushEncryptionDraft03& operator=(const WebPushEncryptionDraft03&) = delete;
+
+ ~WebPushEncryptionDraft03() override = default;
+
+ // GCMMessageCryptographer::EncryptionScheme implementation.
+ std::string DerivePseudoRandomKey(
+ const base::StringPiece& /* recipient_public_key */,
+ const base::StringPiece& /* sender_public_key */,
+ const base::StringPiece& ecdh_shared_secret,
+ const base::StringPiece& auth_secret) override {
+ const char kInfo[] = "Content-Encoding: auth";
+
+ // This deliberately copies over the NUL terminus.
+ base::StringPiece info(kInfo, sizeof(kInfo));
+
+ return crypto::HkdfSha256(ecdh_shared_secret, auth_secret, info, 32);
+ }
+
+ // Creates the info parameter for an HKDF value for the given
+ // |content_encoding| in accordance with draft-ietf-webpush-encryption-03.
+ //
+ // cek_info = "Content-Encoding: aesgcm" || 0x00 || context
+ // nonce_info = "Content-Encoding: nonce" || 0x00 || context
+ //
+ // context = "P-256" || 0x00 ||
+ // length(recipient_public) || recipient_public ||
+ // length(sender_public) || sender_public
+ //
+ // The length of the public keys must be written as a two octet unsigned
+ // integer in network byte order (big endian).
+ std::string GenerateInfoForContentEncoding(
+ EncodingType type,
+ const base::StringPiece& recipient_public_key,
+ const base::StringPiece& sender_public_key) override {
+ std::stringstream info_stream;
+ info_stream << "Content-Encoding: ";
+
+ switch (type) {
+ case EncodingType::CONTENT_ENCRYPTION_KEY:
+ info_stream << "aesgcm";
+ break;
+ case EncodingType::NONCE:
+ info_stream << "nonce";
+ break;
+ }
+
+ info_stream << '\x00' << "P-256" << '\x00';
+
+ uint16_t local_len =
+ base::HostToNet16(static_cast<uint16_t>(recipient_public_key.size()));
+ info_stream.write(reinterpret_cast<char*>(&local_len), sizeof(local_len));
+ info_stream << recipient_public_key;
+
+ uint16_t peer_len =
+ base::HostToNet16(static_cast<uint16_t>(sender_public_key.size()));
+ info_stream.write(reinterpret_cast<char*>(&peer_len), sizeof(peer_len));
+ info_stream << sender_public_key;
+
+ return info_stream.str();
+ }
+
+ // draft-ietf-webpush-encryption-03 defines that the padding is included at
+ // the beginning of the message. The first two bytes, in network byte order,
+ // contain the length of the included padding. Then that exact number of bytes
+ // must follow as padding, all of which must have a zero value.
+ //
+ // TODO(peter): Add support for message padding if the GCMMessageCryptographer
+ // starts encrypting payloads for reasons other than testing.
+ std::string CreateRecord(const base::StringPiece& plaintext) override {
+ std::string record;
+ record.reserve(sizeof(uint16_t) + plaintext.size());
+ record.append(sizeof(uint16_t), '\x00');
+ record.append(plaintext.data(), plaintext.size());
+ return record;
+ }
+
+ // The |ciphertext| must be at least of size kAuthenticationTagBytes with two
+ // padding bytes, which is the case for an empty message with zero padding.
+ // The |record_size| must be large enough to use only one record.
+ // https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-03#section-2
+ bool ValidateCiphertextSize(size_t ciphertext_size,
+ size_t record_size) override {
+ return ciphertext_size >=
+ sizeof(uint16_t) +
+ GCMMessageCryptographer::kAuthenticationTagBytes &&
+ ciphertext_size <=
+ record_size + GCMMessageCryptographer::kAuthenticationTagBytes;
+ }
+
+ // The record padding in draft-ietf-webpush-encryption-03 is included at the
+ // beginning of the record. The first two bytes indicate the length of the
+ // padding. All padding bytes immediately follow, and must be set to zero.
+ bool ValidateAndRemovePadding(base::StringPiece& record) override {
+ // Records must be at least two octets in size (to hold the padding).
+ // Records that are smaller, i.e. a single octet, are invalid.
+ if (record.size() < sizeof(uint16_t))
+ return false;
+
+ // Records contain a two-byte, big-endian padding length followed by zero to
+ // 65535 bytes of padding. Padding bytes must be zero but, since AES-GCM
+ // authenticates the plaintext, checking and removing padding need not be
+ // done in constant-time.
+ uint16_t padding_length = (static_cast<uint8_t>(record[0]) << 8) |
+ static_cast<uint8_t>(record[1]);
+ record.remove_prefix(sizeof(uint16_t));
+
+ if (padding_length > record.size()) {
+ return false;
+ }
+
+ for (size_t i = 0; i < padding_length; ++i) {
+ if (record[i] != 0)
+ return false;
+ }
+
+ record.remove_prefix(padding_length);
+ return true;
+ }
+};
+
+// Implementation of draft 08 of the Web Push Encryption standard:
+// https://tools.ietf.org/html/draft-ietf-webpush-encryption-08
+// https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-07
+class WebPushEncryptionDraft08
+ : public GCMMessageCryptographer::EncryptionScheme {
+ public:
+ WebPushEncryptionDraft08() = default;
+
+ WebPushEncryptionDraft08(const WebPushEncryptionDraft08&) = delete;
+ WebPushEncryptionDraft08& operator=(const WebPushEncryptionDraft08&) = delete;
+
+ ~WebPushEncryptionDraft08() override = default;
+
+ // GCMMessageCryptographer::EncryptionScheme implementation.
+ std::string DerivePseudoRandomKey(
+ const base::StringPiece& recipient_public_key,
+ const base::StringPiece& sender_public_key,
+ const base::StringPiece& ecdh_shared_secret,
+ const base::StringPiece& auth_secret) override {
+ DCHECK_EQ(recipient_public_key.size(), 65u);
+ DCHECK_EQ(sender_public_key.size(), 65u);
+
+ const char kInfo[] = "WebPush: info";
+
+ // This deliberately copies over the NUL terminus.
+ std::string info = base::StrCat({base::StringPiece(kInfo, sizeof(kInfo)),
+ recipient_public_key, sender_public_key});
+
+ return crypto::HkdfSha256(ecdh_shared_secret, auth_secret, info, 32);
+ }
+
+ // The info string used for generating the content encryption key and the
+ // nonce was simplified in draft-ietf-webpush-encryption-08, because the
+ // public keys of both the recipient and the sender are now in the PRK.
+ std::string GenerateInfoForContentEncoding(
+ EncodingType type,
+ const base::StringPiece& /* recipient_public_key */,
+ const base::StringPiece& /* sender_public_key */) override {
+ std::stringstream info_stream;
+ info_stream << "Content-Encoding: ";
+
+ switch (type) {
+ case EncodingType::CONTENT_ENCRYPTION_KEY:
+ info_stream << "aes128gcm";
+ break;
+ case EncodingType::NONCE:
+ info_stream << "nonce";
+ break;
+ }
+
+ info_stream << '\x00';
+ return info_stream.str();
+ }
+
+ // draft-ietf-webpush-encryption-08 defines that the padding follows the
+ // plaintext of a message. A delimiter byte (0x02 for the final record) will
+ // be added, and then zero or more bytes of padding.
+ //
+ // TODO(peter): Add support for message padding if the GCMMessageCryptographer
+ // starts encrypting payloads for reasons other than testing.
+ std::string CreateRecord(const base::StringPiece& plaintext) override {
+ std::string record;
+ record.reserve(plaintext.size() + sizeof(uint8_t));
+ record.append(plaintext.data(), plaintext.size());
+ record.append(sizeof(uint8_t), '\x02');
+ return record;
+ }
+
+ // The |ciphertext| must be at least of size kAuthenticationTagBytes with one
+ // padding delimiter, which is the case for an empty message with minimal
+ // padding. The |record_size| must be large enough to use only one record.
+ // https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-08#section-2
+ bool ValidateCiphertextSize(size_t ciphertext_size,
+ size_t record_size) override {
+ return ciphertext_size >=
+ sizeof(uint8_t) +
+ GCMMessageCryptographer::kAuthenticationTagBytes &&
+ ciphertext_size <=
+ record_size + GCMMessageCryptographer::kAuthenticationTagBytes;
+ }
+
+ // The record padding in draft-ietf-webpush-encryption-08 is included at the
+ // end of the record. The length is not defined, but all padding bytes must be
+ // zero until the delimiter (0x02) is found.
+ bool ValidateAndRemovePadding(base::StringPiece& record) override {
+ DCHECK_GE(record.size(), 1u);
+
+ size_t padding_length = 1;
+ for (; padding_length <= record.size(); ++padding_length) {
+ size_t offset = record.size() - padding_length;
+
+ if (record[offset] == 0x02 /* padding delimiter octet */)
+ break;
+
+ if (record[offset] != 0x00 /* valid padding byte */)
+ return false;
+ }
+
+ record.remove_suffix(padding_length);
+ return true;
+ }
+};
+
+} // namespace
+
+const size_t GCMMessageCryptographer::kAuthenticationTagBytes = 16;
+const size_t GCMMessageCryptographer::kSaltSize = 16;
+
+GCMMessageCryptographer::GCMMessageCryptographer(Version version) {
+ switch (version) {
+ case Version::DRAFT_03:
+ encryption_scheme_ = std::make_unique<WebPushEncryptionDraft03>();
+ return;
+ case Version::DRAFT_08:
+ encryption_scheme_ = std::make_unique<WebPushEncryptionDraft08>();
+ return;
+ }
+
+ NOTREACHED();
+}
+
+GCMMessageCryptographer::~GCMMessageCryptographer() = default;
+
+bool GCMMessageCryptographer::Encrypt(
+ const base::StringPiece& recipient_public_key,
+ const base::StringPiece& sender_public_key,
+ const base::StringPiece& ecdh_shared_secret,
+ const base::StringPiece& auth_secret,
+ const base::StringPiece& salt,
+ const base::StringPiece& plaintext,
+ size_t* record_size,
+ std::string* ciphertext) const {
+ DCHECK_EQ(recipient_public_key.size(), 65u);
+ DCHECK_EQ(sender_public_key.size(), 65u);
+ DCHECK_EQ(ecdh_shared_secret.size(), 32u);
+ DCHECK_EQ(auth_secret.size(), 16u);
+ DCHECK_EQ(salt.size(), 16u);
+ DCHECK(record_size);
+ DCHECK(ciphertext);
+
+ std::string prk = encryption_scheme_->DerivePseudoRandomKey(
+ recipient_public_key, sender_public_key, ecdh_shared_secret, auth_secret);
+
+ std::string content_encryption_key = DeriveContentEncryptionKey(
+ recipient_public_key, sender_public_key, prk, salt);
+ std::string nonce =
+ DeriveNonce(recipient_public_key, sender_public_key, prk, salt);
+
+ std::string record = encryption_scheme_->CreateRecord(plaintext);
+ std::string encrypted_record;
+
+ if (!TransformRecord(Direction::ENCRYPT, record, content_encryption_key,
+ nonce, &encrypted_record)) {
+ return false;
+ }
+
+ // The advertised record size must be at least one more than the padded
+ // plaintext to ensure only one record.
+ *record_size = std::max(kDefaultRecordSize, record.size() + 1);
+
+ ciphertext->swap(encrypted_record);
+ return true;
+}
+
+bool GCMMessageCryptographer::Decrypt(
+ const base::StringPiece& recipient_public_key,
+ const base::StringPiece& sender_public_key,
+ const base::StringPiece& ecdh_shared_secret,
+ const base::StringPiece& auth_secret,
+ const base::StringPiece& salt,
+ const base::StringPiece& ciphertext,
+ size_t record_size,
+ std::string* plaintext) const {
+ DCHECK_EQ(recipient_public_key.size(), 65u);
+ DCHECK_EQ(sender_public_key.size(), 65u);
+ DCHECK_EQ(ecdh_shared_secret.size(), 32u);
+ DCHECK_EQ(auth_secret.size(), 16u);
+ DCHECK_EQ(salt.size(), 16u);
+ DCHECK(plaintext);
+
+ if (record_size <= 1) {
+ LOG(ERROR) << "Invalid record size passed.";
+ return false;
+ }
+
+ std::string prk = encryption_scheme_->DerivePseudoRandomKey(
+ recipient_public_key, sender_public_key, ecdh_shared_secret, auth_secret);
+
+ std::string content_encryption_key = DeriveContentEncryptionKey(
+ recipient_public_key, sender_public_key, prk, salt);
+
+ std::string nonce =
+ DeriveNonce(recipient_public_key, sender_public_key, prk, salt);
+
+ if (!encryption_scheme_->ValidateCiphertextSize(ciphertext.size(),
+ record_size)) {
+ LOG(ERROR) << "Invalid ciphertext size passed.";
+ return false;
+ }
+
+ std::string decrypted_record_string;
+ if (!TransformRecord(Direction::DECRYPT, ciphertext, content_encryption_key,
+ nonce, &decrypted_record_string)) {
+ LOG(ERROR) << "Unable to transform the record.";
+ return false;
+ }
+
+ DCHECK(!decrypted_record_string.empty());
+
+ base::StringPiece decrypted_record(decrypted_record_string);
+ if (!encryption_scheme_->ValidateAndRemovePadding(decrypted_record)) {
+ LOG(ERROR) << "Padding could not be validated or removed.";
+ return false;
+ }
+
+ plaintext->assign(decrypted_record.data(), decrypted_record.size());
+ return true;
+}
+
+bool GCMMessageCryptographer::TransformRecord(Direction direction,
+ const base::StringPiece& input,
+ const base::StringPiece& key,
+ const base::StringPiece& nonce,
+ std::string* output) const {
+ DCHECK(output);
+
+ const EVP_AEAD* aead = EVP_aead_aes_128_gcm();
+
+ EVP_AEAD_CTX context;
+ if (!EVP_AEAD_CTX_init(&context, aead,
+ reinterpret_cast<const uint8_t*>(key.data()),
+ key.size(), EVP_AEAD_DEFAULT_TAG_LENGTH, nullptr)) {
+ return false;
+ }
+
+ base::CheckedNumeric<size_t> maximum_output_length(input.size());
+ if (direction == Direction::ENCRYPT)
+ maximum_output_length += kAuthenticationTagBytes;
+
+ // WriteInto requires the buffer to finish with a NULL-byte.
+ maximum_output_length += 1;
+
+ size_t output_length = 0;
+ uint8_t* raw_output = reinterpret_cast<uint8_t*>(
+ base::WriteInto(output, maximum_output_length.ValueOrDie()));
+
+ EVP_AEAD_CTX_TransformFunction* transform_function =
+ direction == Direction::ENCRYPT ? EVP_AEAD_CTX_seal : EVP_AEAD_CTX_open;
+
+ if (!transform_function(
+ &context, raw_output, &output_length, output->size(),
+ reinterpret_cast<const uint8_t*>(nonce.data()), nonce.size(),
+ reinterpret_cast<const uint8_t*>(input.data()), input.size(),
+ nullptr, 0)) {
+ EVP_AEAD_CTX_cleanup(&context);
+ return false;
+ }
+
+ EVP_AEAD_CTX_cleanup(&context);
+
+ base::CheckedNumeric<size_t> expected_output_length(input.size());
+ if (direction == Direction::ENCRYPT)
+ expected_output_length += kAuthenticationTagBytes;
+ else
+ expected_output_length -= kAuthenticationTagBytes;
+
+ DCHECK_EQ(expected_output_length.ValueOrDie(), output_length);
+
+ output->resize(output_length);
+ return true;
+}
+
+std::string GCMMessageCryptographer::DeriveContentEncryptionKey(
+ const base::StringPiece& recipient_public_key,
+ const base::StringPiece& sender_public_key,
+ const base::StringPiece& ecdh_shared_secret,
+ const base::StringPiece& salt) const {
+ std::string content_encryption_key_info =
+ encryption_scheme_->GenerateInfoForContentEncoding(
+ EncryptionScheme::EncodingType::CONTENT_ENCRYPTION_KEY,
+ recipient_public_key, sender_public_key);
+
+ return crypto::HkdfSha256(ecdh_shared_secret, salt,
+ content_encryption_key_info,
+ kContentEncryptionKeySize);
+}
+
+std::string GCMMessageCryptographer::DeriveNonce(
+ const base::StringPiece& recipient_public_key,
+ const base::StringPiece& sender_public_key,
+ const base::StringPiece& ecdh_shared_secret,
+ const base::StringPiece& salt) const {
+ std::string nonce_info = encryption_scheme_->GenerateInfoForContentEncoding(
+ EncryptionScheme::EncodingType::NONCE, recipient_public_key,
+ sender_public_key);
+
+ // https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-02
+ // defines that the result should be XOR'ed with the record's sequence number,
+ // however, Web Push encryption is limited to a single record per
+ // https://tools.ietf.org/html/draft-ietf-webpush-encryption-03.
+
+ return crypto::HkdfSha256(ecdh_shared_secret, salt, nonce_info, kNonceSize);
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/crypto/gcm_message_cryptographer.h b/chromium/components/gcm_driver/crypto/gcm_message_cryptographer.h
new file mode 100644
index 00000000000..1332708e0d9
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/gcm_message_cryptographer.h
@@ -0,0 +1,167 @@
+// Copyright 2015 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 COMPONENTS_GCM_DRIVER_CRYPTO_GCM_MESSAGE_CRYPTOGRAPHER_H_
+#define COMPONENTS_GCM_DRIVER_CRYPTO_GCM_MESSAGE_CRYPTOGRAPHER_H_
+
+#include <stddef.h>
+#include <stdint.h>
+#include <memory>
+#include <string>
+
+#include "base/compiler_specific.h"
+#include "base/gtest_prod_util.h"
+#include "base/strings/string_piece.h"
+
+namespace gcm {
+
+// Messages delivered through GCM may be encrypted according to the IETF Web
+// Push protocol. We support two versions of ietf-webpush-encryption. The user
+// of this class must pass in the version to use when constructing an instance.
+//
+// https://tools.ietf.org/html/draft-ietf-webpush-encryption-03
+// https://tools.ietf.org/html/draft-ietf-webpush-encryption-08 (WGLC)
+//
+// This class implements the ability to encrypt or decrypt such messages using
+// AEAD_AES_128_GCM with a 16-octet authentication tag. The encrypted payload
+// will be stored in a single record.
+//
+// Note that while this class is not responsible for creating or storing the
+// actual keys, it uses a key derivation function for the actual message
+// encryption/decryption, thus allowing for the safe re-use of keys in multiple
+// messages provided that a cryptographically-strong random salt is used.
+class GCMMessageCryptographer {
+ public:
+ // Size, in bytes, of the authentication tag included in the messages.
+ static const size_t kAuthenticationTagBytes;
+
+ // Salt size, in bytes, that will be used together with the key to create a
+ // unique content encryption key for a given message.
+ static const size_t kSaltSize;
+
+ // Version of the encryption scheme desired by the consumer.
+ enum class Version {
+ // https://tools.ietf.org/html/draft-ietf-webpush-encryption-03
+ DRAFT_03,
+
+ // https://tools.ietf.org/html/draft-ietf-webpush-encryption-08 (WGLC)
+ DRAFT_08
+ };
+
+ // Interface that different versions of the encryption scheme must implement.
+ class EncryptionScheme {
+ public:
+ virtual ~EncryptionScheme() {}
+
+ // Type of encoding to produce in GenerateInfoForContentEncoding().
+ enum class EncodingType { CONTENT_ENCRYPTION_KEY, NONCE };
+
+ // Derives the pseudo random key (PRK) to use for deriving the content
+ // encryption key and the nonce.
+ virtual std::string DerivePseudoRandomKey(
+ const base::StringPiece& recipient_public_key,
+ const base::StringPiece& sender_public_key,
+ const base::StringPiece& ecdh_shared_secret,
+ const base::StringPiece& auth_secret) = 0;
+
+ // Generates the info string used for generating the content encryption key
+ // and the nonce used for the cryptographic transformation.
+ virtual std::string GenerateInfoForContentEncoding(
+ EncodingType type,
+ const base::StringPiece& recipient_public_key,
+ const base::StringPiece& sender_public_key) = 0;
+
+ // Creates an encryption record to contain the given |plaintext|.
+ virtual std::string CreateRecord(const base::StringPiece& plaintext) = 0;
+
+ // Validates that the |ciphertext_size| is valid following the scheme.
+ virtual bool ValidateCiphertextSize(size_t ciphertext_size,
+ size_t record_size) = 0;
+
+ // Verifies that the padding included in |record| is valid and removes it
+ // from the StringPiece. Returns whether the padding was valid.
+ virtual bool ValidateAndRemovePadding(base::StringPiece& record) = 0;
+ };
+
+ // Creates a new cryptographer for |version| of the encryption scheme.
+ explicit GCMMessageCryptographer(Version version);
+ ~GCMMessageCryptographer();
+
+ // Encrypts the |plaintext| in accordance with the Web Push Encryption scheme
+ // this cryptographer represents, storing the result in |*record_size| and
+ // |*ciphertext|. Returns whether encryption was successful.
+ //
+ // |recipient_public_key|: Recipient's key as an uncompressed P-256 EC point.
+ // |sender_public_key|: Sender's key as an uncompressed P-256 EC point.
+ // |ecdh_shared_secret|: 32-byte shared secret between the key pairs.
+ // |auth_secret|: 16-byte prearranged secret between recipient and sender.
+ // |salt|: 16-byte cryptographically secure salt unique to the message.
+ // |plaintext|: The plaintext that is to be encrypted.
+ // |*record_size|: Out parameter in which the record size will be written.
+ // |*ciphertext|: Out parameter in which the ciphertext will be written.
+ bool Encrypt(const base::StringPiece& recipient_public_key,
+ const base::StringPiece& sender_public_key,
+ const base::StringPiece& ecdh_shared_secret,
+ const base::StringPiece& auth_secret,
+ const base::StringPiece& salt,
+ const base::StringPiece& plaintext,
+ size_t* record_size,
+ std::string* ciphertext) const WARN_UNUSED_RESULT;
+
+ // Decrypts the |ciphertext| in accordance with the Web Push Encryption scheme
+ // this cryptographer represents, storing the result in |*plaintext|. Returns
+ // whether decryption was successful.
+ //
+ // |recipient_public_key|: Recipient's key as an uncompressed P-256 EC point.
+ // |sender_public_key|: Sender's key as an uncompressed P-256 EC point.
+ // |ecdh_shared_secret|: 32-byte shared secret between the key pairs.
+ // |auth_secret|: 16-byte prearranged secret between recipient and sender.
+ // |salt|: 16-byte cryptographically secure salt unique to the message.
+ // |ciphertext|: The ciphertext that is to be decrypted.
+ // |record_size|: Size of a single record. Must be larger than or equal to
+ // len(plaintext) plus the ciphertext's overhead (18 bytes).
+ // |*plaintext|: Out parameter in which the plaintext will be written.
+ bool Decrypt(const base::StringPiece& recipient_public_key,
+ const base::StringPiece& sender_public_key,
+ const base::StringPiece& ecdh_shared_secret,
+ const base::StringPiece& auth_secret,
+ const base::StringPiece& salt,
+ const base::StringPiece& ciphertext,
+ size_t record_size,
+ std::string* plaintext) const WARN_UNUSED_RESULT;
+
+ private:
+ FRIEND_TEST_ALL_PREFIXES(GCMMessageCryptographerTest, AuthSecretAffectsPRK);
+ FRIEND_TEST_ALL_PREFIXES(GCMMessageCryptographerTest, InvalidRecordPadding);
+
+ enum class Direction { ENCRYPT, DECRYPT };
+
+ // Derives the content encryption key from |ecdh_shared_secret| and |salt|.
+ std::string DeriveContentEncryptionKey(
+ const base::StringPiece& recipient_public_key,
+ const base::StringPiece& sender_public_key,
+ const base::StringPiece& ecdh_shared_secret,
+ const base::StringPiece& salt) const;
+
+ // Derives the nonce from |ecdh_shared_secret| and |salt|.
+ std::string DeriveNonce(const base::StringPiece& recipient_public_key,
+ const base::StringPiece& sender_public_key,
+ const base::StringPiece& ecdh_shared_secret,
+ const base::StringPiece& salt) const;
+
+ // Private implementation of the encryption and decryption routines.
+ bool TransformRecord(Direction direction,
+ const base::StringPiece& input,
+ const base::StringPiece& key,
+ const base::StringPiece& nonce,
+ std::string* output) const;
+
+ // Implementation of the encryption scheme. Set in the constructor depending
+ // on the version requested by the consumer.
+ std::unique_ptr<EncryptionScheme> encryption_scheme_;
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_CRYPTO_GCM_MESSAGE_CRYPTOGRAPHER_H_
diff --git a/chromium/components/gcm_driver/crypto/gcm_message_cryptographer_unittest.cc b/chromium/components/gcm_driver/crypto/gcm_message_cryptographer_unittest.cc
new file mode 100644
index 00000000000..380e166e682
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/gcm_message_cryptographer_unittest.cc
@@ -0,0 +1,906 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/crypto/gcm_message_cryptographer.h"
+
+#include <memory>
+
+#include "base/base64url.h"
+#include "base/big_endian.h"
+#include "base/cxx17_backports.h"
+#include "base/logging.h"
+#include "base/strings/string_piece.h"
+#include "base/strings/string_util.h"
+#include "components/gcm_driver/crypto/message_payload_parser.h"
+#include "components/gcm_driver/crypto/p256_key_util.h"
+#include "crypto/ec_private_key.h"
+#include "crypto/random.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+
+// Example plaintext data to use in the tests.
+const char kExamplePlaintext[] = "Example plaintext";
+
+// Expected sizes of the different input given to the cryptographer.
+constexpr size_t kEcdhSharedSecretSize = 32;
+constexpr size_t kAuthSecretSize = 16;
+constexpr size_t kSaltSize = 16;
+
+// Keying material for both parties as P-256 EC points. Used to make sure that
+// the test vectors are reproducible.
+const unsigned char kCommonSenderPublicKey[] = {
+ 0x04, 0x05, 0x3C, 0xA1, 0xB9, 0xA5, 0xAB, 0xB8, 0x2D, 0x88, 0x48,
+ 0x82, 0xC9, 0x49, 0x19, 0x91, 0xD5, 0xFD, 0xD1, 0x92, 0xDB, 0xA7,
+ 0x7E, 0x70, 0x48, 0x37, 0x41, 0xCD, 0x90, 0x05, 0x80, 0xDF, 0x65,
+ 0x9A, 0xA1, 0x1A, 0x04, 0xF1, 0x98, 0x25, 0xF2, 0xC2, 0x13, 0x5D,
+ 0xD9, 0x72, 0x35, 0x75, 0x24, 0xF9, 0xFF, 0x25, 0xD1, 0xBC, 0x84,
+ 0x46, 0x4E, 0x88, 0x08, 0x55, 0x70, 0x9F, 0xA7, 0x07, 0xD9};
+static_assert(base::size(kCommonSenderPublicKey) == 65,
+ "Raw P-256 public keys must be 65 bytes in size.");
+
+const unsigned char kCommonRecipientPublicKey[] = {
+ 0x04, 0x35, 0x02, 0x67, 0xB9, 0x10, 0x8F, 0x9B, 0xF1, 0x85, 0xF5,
+ 0x1B, 0xD7, 0xA4, 0xEF, 0xBD, 0x28, 0xB3, 0x11, 0x40, 0xBA, 0xD0,
+ 0xEE, 0xB2, 0x97, 0xDA, 0x6A, 0x93, 0x2D, 0x26, 0x45, 0xBD, 0xB2,
+ 0x9A, 0x9F, 0xB8, 0x19, 0xD8, 0x21, 0x6F, 0x66, 0xE3, 0xF6, 0x0B,
+ 0x74, 0xB2, 0x28, 0x38, 0xDC, 0xA7, 0x8A, 0x58, 0x0D, 0x56, 0x47,
+ 0x3E, 0xD0, 0x5B, 0x5C, 0x93, 0x4E, 0xB3, 0x89, 0x87, 0x64};
+static_assert(base::size(kCommonRecipientPublicKey) == 65,
+ "Raw P-256 public keys must be 65 bytes in size.");
+
+const unsigned char kCommonRecipientPrivateKey[] = {
+ 0x30, 0x81, 0x87, 0x02, 0x01, 0x00, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86,
+ 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D,
+ 0x03, 0x01, 0x07, 0x04, 0x6D, 0x30, 0x6B, 0x02, 0x01, 0x01, 0x04, 0x20,
+ 0x16, 0xCC, 0xB4, 0x37, 0xA3, 0x04, 0x0C, 0x28, 0xDE, 0x56, 0x77, 0x27,
+ 0x0B, 0xD8, 0x1E, 0x82, 0xD7, 0x7F, 0x07, 0xA6, 0x43, 0x6E, 0x70, 0xDD,
+ 0x9C, 0x3C, 0xF1, 0x2C, 0x93, 0xE3, 0x37, 0xD1, 0xA1, 0x44, 0x03, 0x42,
+ 0x00, 0x04, 0x35, 0x02, 0x67, 0xB9, 0x10, 0x8F, 0x9B, 0xF1, 0x85, 0xF5,
+ 0x1B, 0xD7, 0xA4, 0xEF, 0xBD, 0x28, 0xB3, 0x11, 0x40, 0xBA, 0xD0, 0xEE,
+ 0xB2, 0x97, 0xDA, 0x6A, 0x93, 0x2D, 0x26, 0x45, 0xBD, 0xB2, 0x9A, 0x9F,
+ 0xB8, 0x19, 0xD8, 0x21, 0x6F, 0x66, 0xE3, 0xF6, 0x0B, 0x74, 0xB2, 0x28,
+ 0x38, 0xDC, 0xA7, 0x8A, 0x58, 0x0D, 0x56, 0x47, 0x3E, 0xD0, 0x5B, 0x5C,
+ 0x93, 0x4E, 0xB3, 0x89, 0x87, 0x64};
+
+const unsigned char kCommonAuthSecret[] = {0x25, 0xF2, 0xC2, 0xB8, 0x19, 0xD8,
+ 0xFD, 0x35, 0x97, 0xDF, 0xFB, 0x5E,
+ 0xF6, 0x0B, 0xD7, 0xA4};
+static_assert(base::size(kCommonAuthSecret) == 16,
+ "Auth secrets must be 16 bytes in size.");
+
+// Test vectors containing reference input for draft-ietf-webpush-encryption
+// that was created using an separate JavaScript implementation of the draft.
+struct TestVector {
+ const char* const input;
+ const unsigned char ecdh_shared_secret[kEcdhSharedSecretSize];
+ const unsigned char auth_secret[kAuthSecretSize];
+ const unsigned char salt[kSaltSize];
+ size_t record_size;
+ const char* const output;
+};
+
+const TestVector kEncryptionTestVectorsDraft03[] = {
+ // Simple message.
+ {"Hello, world!",
+ {0x0B, 0x32, 0xE2, 0xD1, 0x6A, 0xBF, 0x4F, 0x2C, 0x49, 0xEA, 0xF7,
+ 0x5D, 0x71, 0x7D, 0x89, 0xA9, 0xA7, 0x5E, 0x21, 0xB2, 0xB5, 0x51,
+ 0xE6, 0x4C, 0x08, 0x68, 0xD3, 0x6F, 0x8F, 0x72, 0x7E, 0x14},
+ {0xD3, 0xF2, 0x78, 0xBD, 0x8D, 0xDD, 0x84, 0x99, 0x66, 0x08, 0xD7, 0x0F,
+ 0xBA, 0x9B, 0x60, 0xFC},
+ {0x15, 0x4A, 0xD7, 0x73, 0x92, 0xBD, 0x3B, 0xCF, 0x6F, 0x98, 0xDC, 0x9B,
+ 0x8B, 0x56, 0xFB, 0xBD},
+ 4096,
+ "T4SXCyj84drA6wRaBNLGDMzeyOEBWjsIEkS2ros6Aw"},
+ // Empty message.
+ {"",
+ {0x3F, 0xD8, 0x95, 0x2C, 0xA2, 0x11, 0xBD, 0x7B, 0x57, 0xB2, 0x00,
+ 0xBD, 0x57, 0x68, 0x3F, 0xF0, 0x14, 0x57, 0x5F, 0xB1, 0x9F, 0x15,
+ 0x4F, 0x11, 0xF0, 0x4D, 0xA2, 0xE8, 0x4C, 0xEA, 0x74, 0x3B},
+ {0xB1, 0xE1, 0xC7, 0x32, 0x4C, 0xAA, 0x56, 0x32, 0x68, 0x20, 0x0F, 0x26,
+ 0x3F, 0x48, 0x4D, 0x99},
+ {0xE9, 0x39, 0x45, 0xBC, 0x96, 0x96, 0x88, 0x76, 0xFC, 0xA1, 0xAD, 0xE4,
+ 0x9D, 0x28, 0xF3, 0x73},
+ 4096,
+ "8s-Tzq8Cn_eobL6uEcNDXL7K"}};
+
+const TestVector kEncryptionTestVectorsDraft08[] = {
+ // Simple message.
+ {"Hello, world!",
+ {0x0B, 0x32, 0xE2, 0xD1, 0x6A, 0xBF, 0x4F, 0x2C, 0x49, 0xEA, 0xF7,
+ 0x5D, 0x71, 0x7D, 0x89, 0xA9, 0xA7, 0x5E, 0x21, 0xB2, 0xB5, 0x51,
+ 0xE6, 0x4C, 0x08, 0x68, 0xD3, 0x6F, 0x8F, 0x72, 0x7E, 0x14},
+ {0xD3, 0xF2, 0x78, 0xBD, 0x8D, 0xDD, 0x84, 0x99, 0x66, 0x08, 0xD7, 0x0F,
+ 0xBA, 0x9B, 0x60, 0xFC},
+ {0x15, 0x4A, 0xD7, 0x73, 0x92, 0xBD, 0x3B, 0xCF, 0x6F, 0x98, 0xDC, 0x9B,
+ 0x8B, 0x56, 0xFB, 0xBD},
+ 4096,
+ "3biYN3Aa30D30bKJMdGlEyYPrz7Wg293NYc31rb6"},
+ // Empty message.
+ {"",
+ {0x3F, 0xD8, 0x95, 0x2C, 0xA2, 0x11, 0xBD, 0x7B, 0x57, 0xB2, 0x00,
+ 0xBD, 0x57, 0x68, 0x3F, 0xF0, 0x14, 0x57, 0x5F, 0xB1, 0x9F, 0x15,
+ 0x4F, 0x11, 0xF0, 0x4D, 0xA2, 0xE8, 0x4C, 0xEA, 0x74, 0x3B},
+ {0xB1, 0xE1, 0xC7, 0x32, 0x4C, 0xAA, 0x56, 0x32, 0x68, 0x20, 0x0F, 0x26,
+ 0x3F, 0x48, 0x4D, 0x99},
+ {0xE9, 0x39, 0x45, 0xBC, 0x96, 0x96, 0x88, 0x76, 0xFC, 0xA1, 0xAD, 0xE4,
+ 0x9D, 0x28, 0xF3, 0x73},
+ 4096,
+ "5OXY345WYPyIvsF7hx4swuA"}};
+
+const TestVector kDecryptionTestVectorsDraft03[] = {
+ // Simple message.
+ {"lsemWwzlFoJzoidHCnVuxRiJpotTcYokJHKzmQ2FsA",
+ {0x4D, 0x3A, 0x6C, 0xBA, 0xD8, 0x1D, 0x8E, 0x68, 0x8B, 0xE6, 0x76,
+ 0xA7, 0xFF, 0x60, 0xC7, 0xFE, 0x77, 0xE2, 0x6D, 0x37, 0xF6, 0x12,
+ 0x44, 0xE2, 0x25, 0xFE, 0xE1, 0xD8, 0xCF, 0x8A, 0xA8, 0x33},
+ {0x62, 0x36, 0xAC, 0xCA, 0x74, 0xD4, 0x49, 0x49, 0x6B, 0x27, 0xB4, 0xF7,
+ 0xC1, 0xE5, 0x30, 0x9A},
+ {0x1C, 0xA7, 0xFD, 0x98, 0x1A, 0xE4, 0xA7, 0x92, 0xE1, 0xB6, 0xA1, 0xE3,
+ 0x41, 0x63, 0x87, 0x76},
+ 4096,
+ "Hello, world!"},
+ // Simple message with 16 bytes of padding.
+ {"VQB6Ds-q9xRqyM1tj_gksSgc78vCWEhphZ-NF1E7_yMfPuRRZlC_Xt9_2NsX3SU",
+ {0x8B, 0x38, 0x8E, 0x22, 0xD5, 0xC4, 0xFD, 0x65, 0x8A, 0xBB, 0xD9,
+ 0x58, 0xBD, 0xF5, 0xFF, 0x79, 0xCF, 0x9D, 0xBD, 0x87, 0x16, 0x7E,
+ 0x93, 0x84, 0x20, 0x8E, 0x8D, 0x49, 0x41, 0x7D, 0x8E, 0x8F},
+ {0x3E, 0x65, 0xC7, 0x1F, 0x75, 0x7A, 0x43, 0xC4, 0x78, 0x6C, 0x64, 0x99,
+ 0x49, 0xA0, 0xC4, 0xB2},
+ {0x43, 0x4D, 0x30, 0x8E, 0xE4, 0x76, 0xB5, 0xD0, 0x87, 0xFC, 0x04, 0xD1,
+ 0x2E, 0x35, 0x75, 0x63},
+ 4096,
+ "Hello, world!"},
+ // Empty message.
+ {"xU8a499UHB_-YSV4VOm-JZnT",
+ {0x68, 0x72, 0x3D, 0x13, 0xE7, 0x50, 0xFA, 0x3E, 0xA0, 0x59, 0x33,
+ 0xF1, 0x73, 0xA8, 0xE8, 0xCD, 0x8D, 0xD4, 0x3C, 0xDC, 0xDE, 0x06,
+ 0x35, 0x5F, 0x51, 0xBB, 0xB2, 0x57, 0x97, 0x72, 0x9D, 0xFB},
+ {0x84, 0xB2, 0x2A, 0xE7, 0xC6, 0xC0, 0xCE, 0x5F, 0xAD, 0x37, 0x06, 0x7F,
+ 0xD1, 0xFD, 0x10, 0x87},
+ {0x9B, 0xC5, 0x8D, 0x5F, 0xD6, 0xD2, 0xA6, 0xBD, 0xAF, 0x4B, 0xD9, 0x60,
+ 0xC6, 0xB4, 0x50, 0x0F},
+ 4096,
+ ""},
+ // Message with an invalid record size.
+ {"gfB-_edj7qEVokyVHpkDJN6FVKHnlWs1RCDw5bmrwQ",
+ {0x5F, 0xE1, 0x7C, 0x4B, 0xFF, 0x04, 0xBF, 0x2C, 0x70, 0x67, 0xFA,
+ 0xF8, 0xB0, 0x07, 0x4F, 0xF6, 0x3C, 0x03, 0x6F, 0xBE, 0xA1, 0x1F,
+ 0x4B, 0x99, 0x25, 0x4F, 0xB9, 0x5F, 0xC4, 0x78, 0x76, 0xDE},
+ {0x59, 0xAB, 0x45, 0xFC, 0x6A, 0xF5, 0xB3, 0xE0, 0xF5, 0x40, 0xD7, 0x98,
+ 0x0F, 0xF0, 0xA4, 0xCB},
+ {0xDB, 0xA0, 0xF2, 0x91, 0x8D, 0x50, 0x42, 0xE0, 0x17, 0x68, 0x5B, 0x9B,
+ 0xF2, 0xA2, 0xC3, 0xF9},
+ 7,
+ nullptr},
+ // Message with four bytes of invalid, non-zero padding.
+ {"2FJmrF95yVU8Q8cYQy9OoOwCb59ZoRlxazPE0T-MNOSMbr0",
+ {0x6B, 0x82, 0x92, 0xD3, 0x71, 0x9A, 0x97, 0x76, 0x45, 0x11, 0x99,
+ 0x6D, 0xBF, 0x56, 0xCC, 0x81, 0x98, 0x56, 0x80, 0xF5, 0x78, 0x36,
+ 0xD6, 0x43, 0x95, 0x68, 0xDB, 0x0F, 0x23, 0x39, 0xF3, 0x6E},
+ {0x02, 0x16, 0xDC, 0xC3, 0xDE, 0x2C, 0xB5, 0x08, 0x89, 0xDB, 0xD8, 0x18,
+ 0x68, 0x83, 0x1C, 0xDB},
+ {0xB7, 0x85, 0x5D, 0x8E, 0x84, 0xC3, 0x2D, 0x61, 0x9B, 0x78, 0x3B, 0x60,
+ 0x0E, 0x70, 0x84, 0xF3},
+ 4096,
+ nullptr},
+ // Message with multiple (2) records.
+ {"reI6sW6y67FI8Kxk-x9GNwiu77His_f5GioDBiKS7IzjDQ",
+ {0xC6, 0x16, 0x6F, 0xAF, 0xE1, 0xB6, 0x8F, 0x2B, 0x0F, 0x67, 0x5A,
+ 0xC7, 0xAC, 0x7E, 0xF6, 0x7C, 0x33, 0xA2, 0xA1, 0x11, 0xB0, 0xB0,
+ 0xAB, 0xAC, 0x37, 0x61, 0xF4, 0xCB, 0x98, 0xFF, 0x00, 0x51},
+ {0xAE, 0xDA, 0x86, 0xDF, 0x6B, 0x03, 0x88, 0xDE, 0x90, 0xBB, 0xB7, 0xA0,
+ 0x78, 0x91, 0x3A, 0x36},
+ {0x4C, 0x4E, 0x2A, 0x8D, 0x88, 0x82, 0xCF, 0xC2, 0xF9, 0x8A, 0xFD, 0x31,
+ 0xF8, 0xD1, 0xF6, 0xB5},
+ 8,
+ nullptr}};
+
+const TestVector kDecryptionTestVectorsDraft08[] = {
+ // Simple message.
+ {"baIDPDv-Do_x1RVtlFDex2uCvd3Ugrv-gJG3sWeg",
+ {0x4D, 0x3A, 0x6C, 0xBA, 0xD8, 0x1D, 0x8E, 0x68, 0x8B, 0xE6, 0x76,
+ 0xA7, 0xFF, 0x60, 0xC7, 0xFE, 0x77, 0xE2, 0x6D, 0x37, 0xF6, 0x12,
+ 0x44, 0xE2, 0x25, 0xFE, 0xE1, 0xD8, 0xCF, 0x8A, 0xA8, 0x33},
+ {0x62, 0x36, 0xAC, 0xCA, 0x74, 0xD4, 0x49, 0x49, 0x6B, 0x27, 0xB4, 0xF7,
+ 0xC1, 0xE5, 0x30, 0x9A},
+ {0x1C, 0xA7, 0xFD, 0x98, 0x1A, 0xE4, 0xA7, 0x92, 0xE1, 0xB6, 0xA1, 0xE3,
+ 0x41, 0x63, 0x87, 0x76},
+ 4096,
+ "Hello, world!"},
+ // Simple message with 16 bytes of padding.
+ {"6Zq7GKQ7zRxeOWoYR71Nx7xJzCZUUNhz6bhV1-ZIg6dVra0x1uWXms5gHp6F6A",
+ {0x8B, 0x38, 0x8E, 0x22, 0xD5, 0xC4, 0xFD, 0x65, 0x8A, 0xBB, 0xD9,
+ 0x58, 0xBD, 0xF5, 0xFF, 0x79, 0xCF, 0x9D, 0xBD, 0x87, 0x16, 0x7E,
+ 0x93, 0x84, 0x20, 0x8E, 0x8D, 0x49, 0x41, 0x7D, 0x8E, 0x8F},
+ {0x3E, 0x65, 0xC7, 0x1F, 0x75, 0x7A, 0x43, 0xC4, 0x78, 0x6C, 0x64, 0x99,
+ 0x49, 0xA0, 0xC4, 0xB2},
+ {0x43, 0x4D, 0x30, 0x8E, 0xE4, 0x76, 0xB5, 0xD0, 0x87, 0xFC, 0x04, 0xD1,
+ 0x2E, 0x35, 0x75, 0x63},
+ 4096,
+ "Hello, world!"},
+ // Empty message.
+ {"bHU7ponA7WAGB0onUybG9nQ",
+ {0x68, 0x72, 0x3D, 0x13, 0xE7, 0x50, 0xFA, 0x3E, 0xA0, 0x59, 0x33,
+ 0xF1, 0x73, 0xA8, 0xE8, 0xCD, 0x8D, 0xD4, 0x3C, 0xDC, 0xDE, 0x06,
+ 0x35, 0x5F, 0x51, 0xBB, 0xB2, 0x57, 0x97, 0x72, 0x9D, 0xFB},
+ {0x84, 0xB2, 0x2A, 0xE7, 0xC6, 0xC0, 0xCE, 0x5F, 0xAD, 0x37, 0x06, 0x7F,
+ 0xD1, 0xFD, 0x10, 0x87},
+ {0x9B, 0xC5, 0x8D, 0x5F, 0xD6, 0xD2, 0xA6, 0xBD, 0xAF, 0x4B, 0xD9, 0x60,
+ 0xC6, 0xB4, 0x50, 0x0F},
+ 4096,
+ ""}};
+
+// Computes the shared secret between the sender and the receiver. The sender
+// must have a ASN.1-encoded PKCS #8 EncryptedPrivateKeyInfo block, whereas
+// the receiver must have a public key in uncompressed EC point format.
+bool ComputeSharedP256SecretFromPrivateKeyStr(
+ const base::StringPiece& private_key,
+ const base::StringPiece& peer_public_key,
+ std::string* out_shared_secret) {
+ DCHECK(out_shared_secret);
+ std::unique_ptr<crypto::ECPrivateKey> local_key(
+ crypto::ECPrivateKey::CreateFromPrivateKeyInfo(std::vector<uint8_t>(
+ private_key.data(), private_key.data() + private_key.size())));
+ if (!local_key) {
+ DLOG(ERROR) << "Unable to create the local key";
+ return false;
+ }
+
+ return ComputeSharedP256Secret(*local_key, peer_public_key,
+ out_shared_secret);
+}
+
+void ComputeSharedSecret(
+ const base::StringPiece& encoded_sender_private_key,
+ const base::StringPiece& encoded_receiver_public_key,
+ std::string* shared_secret) {
+ std::string sender_private_key, receiver_public_key;
+ ASSERT_TRUE(base::Base64UrlDecode(
+ encoded_sender_private_key,
+ base::Base64UrlDecodePolicy::IGNORE_PADDING, &sender_private_key));
+ ASSERT_TRUE(base::Base64UrlDecode(
+ encoded_receiver_public_key,
+ base::Base64UrlDecodePolicy::IGNORE_PADDING, &receiver_public_key));
+
+ ASSERT_TRUE(ComputeSharedP256SecretFromPrivateKeyStr(
+ sender_private_key, receiver_public_key,
+ shared_secret));
+}
+
+} // namespace
+
+class GCMMessageCryptographerTestBase : public ::testing::Test {
+ public:
+ void SetUp() override {
+ recipient_public_key_.assign(
+ kCommonRecipientPublicKey,
+ kCommonRecipientPublicKey + base::size(kCommonRecipientPublicKey));
+ sender_public_key_.assign(
+ kCommonSenderPublicKey,
+ kCommonSenderPublicKey + base::size(kCommonSenderPublicKey));
+
+ std::string recipient_private_key(
+ kCommonRecipientPrivateKey,
+ kCommonRecipientPrivateKey + base::size(kCommonRecipientPrivateKey));
+ std::vector<uint8_t> recipient_private_key_vec(
+ recipient_private_key.begin(), recipient_private_key.end());
+ std::unique_ptr<crypto::ECPrivateKey> recipient_key =
+ crypto::ECPrivateKey::CreateFromPrivateKeyInfo(recipient_private_key_vec);
+ ASSERT_TRUE(recipient_key);
+ ASSERT_TRUE(ComputeSharedP256Secret(
+ *recipient_key, sender_public_key_, &ecdh_shared_secret_));
+
+ auth_secret_.assign(kCommonAuthSecret,
+ kCommonAuthSecret + base::size(kCommonAuthSecret));
+ }
+
+ protected:
+ // Public keys of the recipient and sender as uncompressed P-256 EC points.
+ std::string recipient_public_key_;
+ std::string sender_public_key_;
+
+ // Shared secret to use in transformations. Associated with the keys above.
+ std::string ecdh_shared_secret_;
+
+ // Authentication secret to use in tests where no specific value is expected.
+ std::string auth_secret_;
+};
+
+class GCMMessageCryptographerTest
+ : public GCMMessageCryptographerTestBase,
+ public testing::WithParamInterface<GCMMessageCryptographer::Version> {
+ public:
+ void SetUp() override {
+ GCMMessageCryptographerTestBase::SetUp();
+
+ cryptographer_ = std::make_unique<GCMMessageCryptographer>(GetParam());
+ }
+
+ protected:
+ // Generates a cryptographically secure random salt of 16-octets in size, the
+ // required length as expected by the HKDF.
+ std::string GenerateRandomSalt() {
+ std::string salt;
+
+ crypto::RandBytes(base::WriteInto(&salt, kSaltSize + 1), kSaltSize);
+ return salt;
+ }
+
+ // The GCMMessageCryptographer instance to use for the tests.
+ std::unique_ptr<GCMMessageCryptographer> cryptographer_;
+};
+
+TEST_P(GCMMessageCryptographerTest, RoundTrip) {
+ const std::string salt = GenerateRandomSalt();
+
+ size_t record_size = 0;
+
+ std::string ciphertext, plaintext;
+ ASSERT_TRUE(cryptographer_->Encrypt(
+ recipient_public_key_, sender_public_key_, ecdh_shared_secret_,
+ auth_secret_, salt, kExamplePlaintext, &record_size, &ciphertext));
+
+ EXPECT_GT(record_size, ciphertext.size() - 16);
+ EXPECT_GT(ciphertext.size(), 0u);
+
+ ASSERT_TRUE(cryptographer_->Decrypt(recipient_public_key_, sender_public_key_,
+ ecdh_shared_secret_, auth_secret_, salt,
+ ciphertext, record_size, &plaintext));
+
+ EXPECT_EQ(kExamplePlaintext, plaintext);
+}
+
+TEST_P(GCMMessageCryptographerTest, RoundTripEmptyMessage) {
+ const std::string salt = GenerateRandomSalt();
+ const std::string message;
+
+ size_t record_size = 0;
+
+ std::string ciphertext, plaintext;
+ ASSERT_TRUE(cryptographer_->Encrypt(recipient_public_key_, sender_public_key_,
+ ecdh_shared_secret_, auth_secret_, salt,
+ message, &record_size, &ciphertext));
+
+ EXPECT_GT(record_size, ciphertext.size() - 16);
+ EXPECT_GT(ciphertext.size(), 0u);
+
+ ASSERT_TRUE(cryptographer_->Decrypt(recipient_public_key_, sender_public_key_,
+ ecdh_shared_secret_, auth_secret_, salt,
+ ciphertext, record_size, &plaintext));
+
+ EXPECT_EQ(message, plaintext);
+}
+
+TEST_P(GCMMessageCryptographerTest, InvalidRecordSize) {
+ const std::string salt = GenerateRandomSalt();
+
+ size_t record_size = 0;
+
+ std::string ciphertext, plaintext;
+ ASSERT_TRUE(cryptographer_->Encrypt(
+ recipient_public_key_, sender_public_key_, ecdh_shared_secret_,
+ auth_secret_, salt, kExamplePlaintext, &record_size, &ciphertext));
+
+ EXPECT_GT(record_size, ciphertext.size() - 16);
+
+ EXPECT_FALSE(cryptographer_->Decrypt(
+ recipient_public_key_, sender_public_key_, ecdh_shared_secret_,
+ auth_secret_, salt, ciphertext, 0 /* record_size */, &plaintext));
+
+ EXPECT_FALSE(cryptographer_->Decrypt(
+ recipient_public_key_, sender_public_key_, ecdh_shared_secret_,
+ auth_secret_, salt, ciphertext, ciphertext.size() - 17, &plaintext));
+
+ EXPECT_TRUE(cryptographer_->Decrypt(
+ recipient_public_key_, sender_public_key_, ecdh_shared_secret_,
+ auth_secret_, salt, ciphertext, ciphertext.size() - 16, &plaintext));
+}
+
+TEST_P(GCMMessageCryptographerTest, InvalidRecordPadding) {
+ std::string message;
+ switch (GetParam()) {
+ case GCMMessageCryptographer::Version::DRAFT_03:
+ message.append(sizeof(uint8_t), '\00'); // padding length octets
+ message.append(sizeof(uint8_t), '\01');
+
+ message.append(sizeof(uint8_t), '\00'); // padding octet
+ message.append(kExamplePlaintext);
+ break;
+ case GCMMessageCryptographer::Version::DRAFT_08:
+ message.append(kExamplePlaintext);
+ message.append(sizeof(uint8_t), '\x02'); // padding delimiter octet
+ message.append(sizeof(uint8_t), '\x00'); // padding octet
+ break;
+ }
+
+ const std::string salt = GenerateRandomSalt();
+
+ const std::string prk =
+ cryptographer_->encryption_scheme_->DerivePseudoRandomKey(
+ recipient_public_key_, sender_public_key_, ecdh_shared_secret_,
+ auth_secret_);
+ const std::string content_encryption_key =
+ cryptographer_->DeriveContentEncryptionKey(recipient_public_key_,
+ sender_public_key_, prk, salt);
+ const std::string nonce = cryptographer_->DeriveNonce(
+ recipient_public_key_, sender_public_key_, prk, salt);
+
+ ASSERT_GT(message.size(), 2u);
+ const size_t record_size = message.size() + 1;
+
+ std::string ciphertext, plaintext;
+ ASSERT_TRUE(cryptographer_->TransformRecord(
+ GCMMessageCryptographer::Direction::ENCRYPT, message,
+ content_encryption_key, nonce, &ciphertext));
+
+ ASSERT_TRUE(cryptographer_->Decrypt(recipient_public_key_, sender_public_key_,
+ ecdh_shared_secret_, auth_secret_, salt,
+ ciphertext, record_size, &plaintext));
+
+ // Note that GCMMessageCryptographer::Decrypt removes the padding.
+ EXPECT_EQ(kExamplePlaintext, plaintext);
+
+ // Now run the same steps again, but have invalid padding length indicators.
+ // (Only applicable to draft-ietf-webpush-encryption-03.)
+ if (GetParam() == GCMMessageCryptographer::Version::DRAFT_03) {
+ // Padding that will spill over in the payload.
+ {
+ message[1] = 4;
+
+ ASSERT_TRUE(cryptographer_->TransformRecord(
+ GCMMessageCryptographer::Direction::ENCRYPT, message,
+ content_encryption_key, nonce, &ciphertext));
+
+ ASSERT_FALSE(cryptographer_->Decrypt(
+ recipient_public_key_, sender_public_key_, ecdh_shared_secret_,
+ auth_secret_, salt, ciphertext, record_size, &plaintext));
+ }
+
+ // More padding octets than the length of the message.
+ {
+ message[1] = 64;
+
+ ASSERT_TRUE(cryptographer_->TransformRecord(
+ GCMMessageCryptographer::Direction::ENCRYPT, message,
+ content_encryption_key, nonce, &ciphertext));
+
+ ASSERT_FALSE(cryptographer_->Decrypt(
+ recipient_public_key_, sender_public_key_, ecdh_shared_secret_,
+ auth_secret_, salt, ciphertext, record_size, &plaintext));
+ }
+
+ // Correct the |message| to be valid again. (A single byte of padding.)
+ message[1] = 1;
+ }
+
+ // Run tests for a missing delimiter in the record.
+ // (Only applicable to draft-ietf-webpush-encryption-03.)
+ if (GetParam() == GCMMessageCryptographer::Version::DRAFT_08) {
+ message[message.size() - 2] = 0x00;
+
+ ASSERT_TRUE(cryptographer_->TransformRecord(
+ GCMMessageCryptographer::Direction::ENCRYPT, message,
+ content_encryption_key, nonce, &ciphertext));
+
+ ASSERT_FALSE(cryptographer_->Decrypt(
+ recipient_public_key_, sender_public_key_, ecdh_shared_secret_,
+ auth_secret_, salt, ciphertext, record_size, &plaintext));
+
+ // Correct the |message| to be valid again. (Proper padding delimiter.)
+ message[message.size() - 2] = 0x02;
+ }
+
+ // Finally run a test to make sure that we validate that all padding bytes are
+ // set to zeros. The position of the padding byte depends on the version.
+ switch (GetParam()) {
+ case GCMMessageCryptographer::Version::DRAFT_03:
+ message[2] = 0x13;
+ break;
+ case GCMMessageCryptographer::Version::DRAFT_08:
+ message[message.size() - 1] = 0x13;
+ break;
+ }
+
+ ASSERT_TRUE(cryptographer_->TransformRecord(
+ GCMMessageCryptographer::Direction::ENCRYPT, message,
+ content_encryption_key, nonce, &ciphertext));
+
+ ASSERT_FALSE(cryptographer_->Decrypt(
+ recipient_public_key_, sender_public_key_, ecdh_shared_secret_,
+ auth_secret_, salt, ciphertext, record_size, &plaintext));
+}
+
+TEST_P(GCMMessageCryptographerTest, AuthSecretAffectsPRK) {
+ std::string first_auth_secret, second_auth_secret;
+
+ crypto::RandBytes(base::WriteInto(&first_auth_secret, kAuthSecretSize + 1),
+ kAuthSecretSize);
+ crypto::RandBytes(base::WriteInto(&second_auth_secret, kAuthSecretSize + 1),
+ kAuthSecretSize);
+
+ ASSERT_NE(cryptographer_->encryption_scheme_->DerivePseudoRandomKey(
+ recipient_public_key_, sender_public_key_, ecdh_shared_secret_,
+ first_auth_secret),
+ cryptographer_->encryption_scheme_->DerivePseudoRandomKey(
+ recipient_public_key_, sender_public_key_, ecdh_shared_secret_,
+ second_auth_secret));
+
+ std::string salt = GenerateRandomSalt();
+
+ // Verify that the IKM actually gets used by the transformations.
+ size_t first_record_size, second_record_size;
+ std::string first_ciphertext, second_ciphertext;
+
+ ASSERT_TRUE(cryptographer_->Encrypt(recipient_public_key_, sender_public_key_,
+ ecdh_shared_secret_, first_auth_secret,
+ salt, kExamplePlaintext,
+ &first_record_size, &first_ciphertext));
+
+ ASSERT_TRUE(cryptographer_->Encrypt(recipient_public_key_, sender_public_key_,
+ ecdh_shared_secret_, second_auth_secret,
+ salt, kExamplePlaintext,
+ &second_record_size, &second_ciphertext));
+
+ // If the ciphertexts differ despite the same key and salt, it got used.
+ ASSERT_NE(first_ciphertext, second_ciphertext);
+ EXPECT_EQ(first_record_size, second_record_size);
+
+ // Verify that the different ciphertexts can also be translated back to the
+ // plaintext content. This will fail if the auth secret isn't considered.
+ std::string first_plaintext, second_plaintext;
+
+ ASSERT_TRUE(cryptographer_->Decrypt(recipient_public_key_, sender_public_key_,
+ ecdh_shared_secret_, first_auth_secret,
+ salt, first_ciphertext, first_record_size,
+ &first_plaintext));
+
+ ASSERT_TRUE(cryptographer_->Decrypt(recipient_public_key_, sender_public_key_,
+ ecdh_shared_secret_, second_auth_secret,
+ salt, second_ciphertext,
+ second_record_size, &second_plaintext));
+
+ EXPECT_EQ(kExamplePlaintext, first_plaintext);
+ EXPECT_EQ(kExamplePlaintext, second_plaintext);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ GCMMessageCryptographerTestBase,
+ GCMMessageCryptographerTest,
+ ::testing::Values(GCMMessageCryptographer::Version::DRAFT_03,
+ GCMMessageCryptographer::Version::DRAFT_08));
+
+class GCMMessageCryptographerTestVectorTest
+ : public GCMMessageCryptographerTestBase {};
+
+TEST_F(GCMMessageCryptographerTestVectorTest, EncryptionVectorsDraft03) {
+ GCMMessageCryptographer cryptographer(
+ GCMMessageCryptographer::Version::DRAFT_03);
+
+ std::string ecdh_shared_secret, auth_secret, salt, ciphertext, output;
+ size_t record_size = 0;
+
+ for (size_t i = 0; i < base::size(kEncryptionTestVectorsDraft03); ++i) {
+ SCOPED_TRACE(i);
+
+ ecdh_shared_secret.assign(
+ kEncryptionTestVectorsDraft03[i].ecdh_shared_secret,
+ kEncryptionTestVectorsDraft03[i].ecdh_shared_secret +
+ kEcdhSharedSecretSize);
+
+ auth_secret.assign(
+ kEncryptionTestVectorsDraft03[i].auth_secret,
+ kEncryptionTestVectorsDraft03[i].auth_secret + kAuthSecretSize);
+
+ salt.assign(kEncryptionTestVectorsDraft03[i].salt,
+ kEncryptionTestVectorsDraft03[i].salt + kSaltSize);
+
+ ASSERT_TRUE(cryptographer.Encrypt(recipient_public_key_, sender_public_key_,
+ ecdh_shared_secret, auth_secret, salt,
+ kEncryptionTestVectorsDraft03[i].input,
+ &record_size, &ciphertext));
+
+ base::Base64UrlEncode(ciphertext, base::Base64UrlEncodePolicy::OMIT_PADDING,
+ &output);
+
+ EXPECT_EQ(kEncryptionTestVectorsDraft03[i].record_size, record_size);
+ EXPECT_EQ(kEncryptionTestVectorsDraft03[i].output, output);
+ }
+}
+
+TEST_F(GCMMessageCryptographerTestVectorTest, DecryptionVectorsDraft03) {
+ GCMMessageCryptographer cryptographer(
+ GCMMessageCryptographer::Version::DRAFT_03);
+
+ std::string input, ecdh_shared_secret, auth_secret, salt, plaintext;
+ for (size_t i = 0; i < base::size(kDecryptionTestVectorsDraft03); ++i) {
+ SCOPED_TRACE(i);
+
+ ASSERT_TRUE(base::Base64UrlDecode(
+ kDecryptionTestVectorsDraft03[i].input,
+ base::Base64UrlDecodePolicy::IGNORE_PADDING, &input));
+
+ ecdh_shared_secret.assign(
+ kDecryptionTestVectorsDraft03[i].ecdh_shared_secret,
+ kDecryptionTestVectorsDraft03[i].ecdh_shared_secret +
+ kEcdhSharedSecretSize);
+
+ auth_secret.assign(
+ kDecryptionTestVectorsDraft03[i].auth_secret,
+ kDecryptionTestVectorsDraft03[i].auth_secret + kAuthSecretSize);
+
+ salt.assign(kDecryptionTestVectorsDraft03[i].salt,
+ kDecryptionTestVectorsDraft03[i].salt + kSaltSize);
+
+ const bool has_output = kDecryptionTestVectorsDraft03[i].output;
+ const bool result = cryptographer.Decrypt(
+ recipient_public_key_, sender_public_key_, ecdh_shared_secret,
+ auth_secret, salt, input, kDecryptionTestVectorsDraft03[i].record_size,
+ &plaintext);
+
+ if (!has_output) {
+ EXPECT_FALSE(result);
+ continue;
+ }
+
+ EXPECT_TRUE(result);
+ EXPECT_EQ(kDecryptionTestVectorsDraft03[i].output, plaintext);
+ }
+}
+
+TEST_F(GCMMessageCryptographerTestVectorTest, EncryptionVectorsDraft08) {
+ GCMMessageCryptographer cryptographer(
+ GCMMessageCryptographer::Version::DRAFT_08);
+
+ std::string ecdh_shared_secret, auth_secret, salt, ciphertext, output;
+ size_t record_size = 0;
+
+ for (size_t i = 0; i < base::size(kEncryptionTestVectorsDraft08); ++i) {
+ SCOPED_TRACE(i);
+
+ ecdh_shared_secret.assign(
+ kEncryptionTestVectorsDraft08[i].ecdh_shared_secret,
+ kEncryptionTestVectorsDraft08[i].ecdh_shared_secret +
+ kEcdhSharedSecretSize);
+
+ auth_secret.assign(
+ kEncryptionTestVectorsDraft08[i].auth_secret,
+ kEncryptionTestVectorsDraft08[i].auth_secret + kAuthSecretSize);
+
+ salt.assign(kEncryptionTestVectorsDraft08[i].salt,
+ kEncryptionTestVectorsDraft08[i].salt + kSaltSize);
+
+ ASSERT_TRUE(cryptographer.Encrypt(recipient_public_key_, sender_public_key_,
+ ecdh_shared_secret, auth_secret, salt,
+ kEncryptionTestVectorsDraft08[i].input,
+ &record_size, &ciphertext));
+
+ base::Base64UrlEncode(ciphertext, base::Base64UrlEncodePolicy::OMIT_PADDING,
+ &output);
+
+ EXPECT_EQ(kEncryptionTestVectorsDraft08[i].record_size, record_size);
+ EXPECT_EQ(kEncryptionTestVectorsDraft08[i].output, output);
+ }
+}
+
+TEST_F(GCMMessageCryptographerTestVectorTest, DecryptionVectorsDraft08) {
+ GCMMessageCryptographer cryptographer(
+ GCMMessageCryptographer::Version::DRAFT_08);
+
+ std::string input, ecdh_shared_secret, auth_secret, salt, plaintext;
+
+ for (size_t i = 0; i < base::size(kDecryptionTestVectorsDraft08); ++i) {
+ SCOPED_TRACE(i);
+
+ ASSERT_TRUE(base::Base64UrlDecode(
+ kDecryptionTestVectorsDraft08[i].input,
+ base::Base64UrlDecodePolicy::IGNORE_PADDING, &input));
+
+ ecdh_shared_secret.assign(
+ kDecryptionTestVectorsDraft08[i].ecdh_shared_secret,
+ kDecryptionTestVectorsDraft08[i].ecdh_shared_secret +
+ kEcdhSharedSecretSize);
+
+ auth_secret.assign(
+ kDecryptionTestVectorsDraft08[i].auth_secret,
+ kDecryptionTestVectorsDraft08[i].auth_secret + kAuthSecretSize);
+
+ salt.assign(kDecryptionTestVectorsDraft08[i].salt,
+ kDecryptionTestVectorsDraft08[i].salt + kSaltSize);
+
+ const bool has_output = kDecryptionTestVectorsDraft08[i].output;
+ const bool result = cryptographer.Decrypt(
+ recipient_public_key_, sender_public_key_, ecdh_shared_secret,
+ auth_secret, salt, input, kDecryptionTestVectorsDraft08[i].record_size,
+ &plaintext);
+
+ if (!has_output) {
+ EXPECT_FALSE(result);
+ continue;
+ }
+
+ EXPECT_TRUE(result);
+ EXPECT_EQ(kDecryptionTestVectorsDraft08[i].output, plaintext);
+ }
+}
+
+class GCMMessageCryptographerReferenceTest : public ::testing::Test {};
+
+// Reference test included for the Version::DRAFT_03 implementation.
+// https://tools.ietf.org/html/draft-ietf-webpush-encryption-03
+// https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-02
+TEST_F(GCMMessageCryptographerReferenceTest, ReferenceDraft03) {
+ // The 16-byte salt unique to the message.
+ const char kSalt[] = "lngarbyKfMoi9Z75xYXmkg";
+
+ // The 16-byte prearranged secret between the sender and receiver.
+ const char kAuthSecret[] = "R29vIGdvbyBnJyBqb29iIQ";
+
+ // The keying material used by the sender to encrypt the |kCiphertext|.
+ const char kSenderPrivate[] =
+ "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgnCScek-QpEjmOOlT-rQ38nZz"
+ "vdPlqa00Zy0i6m2OJvahRANCAATaEQ22_OCRpvIOWeQhcbq0qrF1iddSLX1xFmFSxPOWOwmJ"
+ "A417CBHOGqsWGkNRvAapFwiegz6Q61rXVo_5roB1";
+ const char kSenderPublicKeyUncompressed[] =
+ "BNoRDbb84JGm8g5Z5CFxurSqsXWJ11ItfXEWYVLE85Y7CYkDjXsIEc4aqxYaQ1G8BqkXCJ6D"
+ "PpDrWtdWj_mugHU";
+
+ // The keying material used by the recipient to decrypt the |kCiphertext|.
+ const char kRecipientPrivate[] =
+ "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg9FWl15_QUQAWDaD3k3l50ZBZ"
+ "QJ4au27F1V4F0uLSD_OhRANCAAQhJAY8y_GdwvqItkO6BObdjafqe6LIxi4Pd6lD9ML6kU9t"
+ "RBFsn9HEA0HGpEDKs-IUCmDkN4pdpzWXLeB4AFEF";
+ const char kRecipientPublicKeyUncompressed[] =
+ "BCEkBjzL8Z3C-oi2Q7oE5t2Np-p7osjGLg93qUP0wvqRT21EEWyf0cQDQcakQMqz4hQKYOQ3"
+ "il2nNZct4HgAUQU";
+
+ // The ciphertext and associated plaintext of the message.
+ const char kCiphertext[] = "6nqAQUME8hNqw5J3kl8cpVVJylXKYqZOeseZG8UueKpA";
+ const char kPlaintext[] = "I am the walrus";
+
+ std::string sender_shared_secret, receiver_shared_secret;
+
+ // Compute the shared secrets between the sender and receiver's keys.
+ ASSERT_NO_FATAL_FAILURE(ComputeSharedSecret(
+ kSenderPrivate, kRecipientPublicKeyUncompressed, &sender_shared_secret));
+ ASSERT_NO_FATAL_FAILURE(ComputeSharedSecret(kRecipientPrivate,
+ kSenderPublicKeyUncompressed,
+ &receiver_shared_secret));
+
+ ASSERT_GT(sender_shared_secret.size(), 0u);
+ ASSERT_EQ(sender_shared_secret, receiver_shared_secret);
+
+ // Decode the public keys of both parties, the auth secret and the salt.
+ std::string recipient_public_key, sender_public_key, auth_secret, salt;
+ ASSERT_TRUE(base::Base64UrlDecode(kRecipientPublicKeyUncompressed,
+ base::Base64UrlDecodePolicy::IGNORE_PADDING,
+ &recipient_public_key));
+ ASSERT_TRUE(base::Base64UrlDecode(kSenderPublicKeyUncompressed,
+ base::Base64UrlDecodePolicy::IGNORE_PADDING,
+ &sender_public_key));
+ ASSERT_TRUE(base::Base64UrlDecode(
+ kAuthSecret, base::Base64UrlDecodePolicy::IGNORE_PADDING, &auth_secret));
+ ASSERT_TRUE(base::Base64UrlDecode(
+ kSalt, base::Base64UrlDecodePolicy::IGNORE_PADDING, &salt));
+
+ std::string encoded_ciphertext, ciphertext, plaintext;
+ size_t record_size = 0;
+
+ // Now verify that encrypting a message with the given information yields the
+ // expected ciphertext given the defined input.
+ GCMMessageCryptographer cryptographer(
+ GCMMessageCryptographer::Version::DRAFT_03);
+
+ ASSERT_TRUE(cryptographer.Encrypt(recipient_public_key, sender_public_key,
+ sender_shared_secret, auth_secret, salt,
+ kPlaintext, &record_size, &ciphertext));
+
+ base::Base64UrlEncode(ciphertext, base::Base64UrlEncodePolicy::OMIT_PADDING,
+ &encoded_ciphertext);
+ ASSERT_EQ(kCiphertext, encoded_ciphertext);
+
+ // And verify that decrypting the message yields the plaintext again.
+ ASSERT_TRUE(cryptographer.Decrypt(recipient_public_key, sender_public_key,
+ sender_shared_secret, auth_secret, salt,
+ ciphertext, record_size, &plaintext));
+
+ ASSERT_EQ(kPlaintext, plaintext);
+}
+
+// Reference test included for the Version::DRAFT_08 implementation.
+// https://tools.ietf.org/html/draft-ietf-webpush-encryption-08
+// https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-07
+TEST_F(GCMMessageCryptographerReferenceTest, ReferenceDraft08) {
+ // The 16-byte prearranged secret between the sender and receiver.
+ const char kAuthSecret[] = "BTBZMqHH6r4Tts7J_aSIgg";
+
+ // The keying material used by the sender to encrypt the |kCiphertext|.
+ const char kSenderPrivate[] =
+ "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgyfWPiYE-n46HLnH0KqZOF1fJ"
+ "JU3MYrct3AELtAQ-oRyhRANCAAT-M_SrDepxkU21WCP3O1SUj0EwbZIHMtu5pZpTKGSCIA5Z"
+ "ent7wmC6HCJ5mFgJkuk5cwAvMBKiiujwa7t45ewP";
+
+ // The keying material used by the recipient to decrypt the |kCiphertext|.
+const char kRecipientPrivate[] =
+ "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgq1dXpw3UpT5VOmu_cf_v6ih0"
+ "7Aems3njxI-JWgLcM96hRANCAAQlcbK-zf3jYFUarx7Q9M02bBHOvlVfiby3sYalMzkXMWjs"
+ "4uvgGFl70wR5uG48j47O1XfKWRh-kkaZDbaCAIsO";
+ const char kRecipientPublicKeyUncompressed[] =
+ "BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZ"
+ "GH6SRpkNtoIAiw4";
+
+ // The plain text of the message, as well as the encrypted reference message.
+ const char kPlaintext[] = "When I grow up, I want to be a watermelon";
+ const char kReferenceMessage[] =
+ "DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_"
+ "c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A_"
+ "yl95bQpu6cVPTpK4Mqgkf1CXztLVBSt2Ks3oZwbuwXPXLWyouBWLVWGNWQexSgSxsj_"
+ "Qulcy4a-fN";
+
+ std::string message;
+ ASSERT_TRUE(base::Base64UrlDecode(kReferenceMessage,
+ base::Base64UrlDecodePolicy::IGNORE_PADDING,
+ &message));
+
+ MessagePayloadParser message_parser(message);
+ ASSERT_TRUE(message_parser.IsValid());
+
+ base::StringPiece salt = message_parser.salt();
+ uint32_t record_size = message_parser.record_size();
+ base::StringPiece sender_public_key = message_parser.public_key();
+ base::StringPiece ciphertext = message_parser.ciphertext();
+
+ std::string sender_shared_secret, receiver_shared_secret;
+
+ // Compute the shared secrets between the sender and receiver's keys.
+ ASSERT_NO_FATAL_FAILURE(ComputeSharedSecret(
+ kSenderPrivate, kRecipientPublicKeyUncompressed, &sender_shared_secret));
+
+ // Compute the shared secret based on the sender's public key, which isn't a
+ // constant but instead is included in the message's binary header.
+ std::string recipient_private_key;
+ ASSERT_TRUE(base::Base64UrlDecode(kRecipientPrivate,
+ base::Base64UrlDecodePolicy::IGNORE_PADDING,
+ &recipient_private_key));
+ ASSERT_NO_FATAL_FAILURE(ComputeSharedP256SecretFromPrivateKeyStr(
+ recipient_private_key, sender_public_key,
+ &receiver_shared_secret));
+
+ ASSERT_GT(sender_shared_secret.size(), 0u);
+ ASSERT_EQ(sender_shared_secret, receiver_shared_secret);
+
+ // Decode the public keys of both parties and the auth secret.
+ std::string recipient_public_key, auth_secret;
+ ASSERT_TRUE(base::Base64UrlDecode(kRecipientPublicKeyUncompressed,
+ base::Base64UrlDecodePolicy::IGNORE_PADDING,
+ &recipient_public_key));
+ ASSERT_TRUE(base::Base64UrlDecode(
+ kAuthSecret, base::Base64UrlDecodePolicy::IGNORE_PADDING, &auth_secret));
+
+ // Attempt to decrypt the message using a GCMMessageCryptographer for this
+ // version of the draft, and then re-encrypt it agian to make sure it matches.
+ GCMMessageCryptographer cryptographer(
+ GCMMessageCryptographer::Version::DRAFT_08);
+
+ std::string plaintext;
+
+ ASSERT_TRUE(cryptographer.Decrypt(recipient_public_key, sender_public_key,
+ sender_shared_secret, auth_secret, salt,
+ ciphertext, record_size, &plaintext));
+ ASSERT_EQ(kPlaintext, plaintext);
+
+ size_t record_size2;
+ std::string ciphertext2;
+
+ ASSERT_TRUE(cryptographer.Encrypt(recipient_public_key, sender_public_key,
+ sender_shared_secret, auth_secret, salt,
+ kPlaintext, &record_size2, &ciphertext2));
+
+ EXPECT_GE(record_size2, record_size);
+ EXPECT_EQ(ciphertext2, ciphertext);
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/crypto/message_payload_parser.cc b/chromium/components/gcm_driver/crypto/message_payload_parser.cc
new file mode 100644
index 00000000000..175c2d8ebd8
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/message_payload_parser.cc
@@ -0,0 +1,77 @@
+// Copyright 2017 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.
+
+#include "components/gcm_driver/crypto/message_payload_parser.h"
+
+#include "base/big_endian.h"
+#include "base/strings/string_piece.h"
+#include "components/gcm_driver/crypto/gcm_decryption_result.h"
+
+namespace gcm {
+
+namespace {
+
+// Size, in bytes, of the salt included in the message header.
+constexpr size_t kSaltSize = 16;
+
+// Size, in bytes, of the uncompressed point included in the message header.
+constexpr size_t kUncompressedPointSize = 65;
+
+// Size, in bytes, of the smallest allowable record_size value.
+constexpr size_t kMinimumRecordSize = 18;
+
+// Size, in bytes, of an empty message with the minimum amount of padding.
+constexpr size_t kMinimumMessageSize =
+ kSaltSize + sizeof(uint32_t) + sizeof(uint8_t) + kUncompressedPointSize +
+ kMinimumRecordSize;
+
+} // namespace
+
+MessagePayloadParser::MessagePayloadParser(base::StringPiece message) {
+ if (message.size() < kMinimumMessageSize) {
+ failure_reason_ = GCMDecryptionResult::INVALID_BINARY_HEADER_PAYLOAD_LENGTH;
+ return;
+ }
+
+ salt_ = std::string(message.substr(0, kSaltSize));
+ message.remove_prefix(kSaltSize);
+
+ base::ReadBigEndian(reinterpret_cast<const uint8_t*>(message.data()),
+ &record_size_);
+ message.remove_prefix(sizeof(record_size_));
+
+ if (record_size_ < kMinimumRecordSize) {
+ failure_reason_ = GCMDecryptionResult::INVALID_BINARY_HEADER_RECORD_SIZE;
+ return;
+ }
+
+ uint8_t public_key_length;
+ base::ReadBigEndian(reinterpret_cast<const uint8_t*>(message.data()),
+ &public_key_length);
+ message.remove_prefix(sizeof(public_key_length));
+
+ if (public_key_length != kUncompressedPointSize) {
+ failure_reason_ =
+ GCMDecryptionResult::INVALID_BINARY_HEADER_PUBLIC_KEY_LENGTH;
+ return;
+ }
+
+ if (message[0] != 0x04) {
+ failure_reason_ =
+ GCMDecryptionResult::INVALID_BINARY_HEADER_PUBLIC_KEY_FORMAT;
+ return;
+ }
+
+ public_key_ = std::string(message.substr(0, kUncompressedPointSize));
+ message.remove_prefix(kUncompressedPointSize);
+
+ ciphertext_ = std::string(message);
+ DCHECK_GE(ciphertext_.size(), kMinimumRecordSize);
+
+ is_valid_ = true;
+}
+
+MessagePayloadParser::~MessagePayloadParser() = default;
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/crypto/message_payload_parser.h b/chromium/components/gcm_driver/crypto/message_payload_parser.h
new file mode 100644
index 00000000000..2810385e048
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/message_payload_parser.h
@@ -0,0 +1,97 @@
+// Copyright 2017 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 COMPONENTS_GCM_DRIVER_CRYPTO_MESSAGE_PAYLOAD_PARSER_H_
+#define COMPONENTS_GCM_DRIVER_CRYPTO_MESSAGE_PAYLOAD_PARSER_H_
+
+#include <stdint.h>
+
+#include "base/check.h"
+#include "base/strings/string_piece.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+
+namespace gcm {
+
+enum class GCMDecryptionResult;
+
+// Parses and validates the binary message payload included in messages that
+// are encrypted per draft-ietf-webpush-encryption-08:
+//
+// https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-08#section-2.1
+//
+// In summary, such messages start with a binary header block that includes the
+// parameters needed to decrypt the content, other than the key. All content
+// following this binary header is considered the ciphertext.
+//
+// +-----------+--------+-----------+-----------------+
+// | salt (16) | rs (4) | idlen (1) | public_key (65) |
+// +-----------+--------+-----------+-----------------+
+//
+// Specific to Web Push encryption, the `public_key` parameter of this header
+// must be set to the ECDH public key of the sender. This is a point on the
+// P-256 elliptic curve in uncompressed form, 65 bytes long starting with 0x04.
+//
+// https://tools.ietf.org/html/draft-ietf-webpush-encryption-08#section-3.1
+class MessagePayloadParser {
+ public:
+ explicit MessagePayloadParser(base::StringPiece message);
+
+ MessagePayloadParser(const MessagePayloadParser&) = delete;
+ MessagePayloadParser& operator=(const MessagePayloadParser&) = delete;
+
+ ~MessagePayloadParser();
+
+ // Returns whether the parser represents a valid message.
+ bool IsValid() const { return is_valid_; }
+
+ // Returns the failure reason when the given payload could not be parsed. Must
+ // only be called when IsValid() returns false.
+ GCMDecryptionResult GetFailureReason() const {
+ DCHECK(failure_reason_.has_value());
+ return failure_reason_.value();
+ }
+
+ // Returns the 16-byte long salt for the message. Must only be called after
+ // validity of the message has been verified.
+ const std::string& salt() const {
+ CHECK(is_valid_);
+ return salt_;
+ }
+
+ // Returns the record size for the message. Must only be called after validity
+ // of the message has been verified.
+ uint32_t record_size() const {
+ CHECK(is_valid_);
+ return record_size_;
+ }
+
+ // Returns the sender's ECDH public key for the message. This will be a point
+ // on the P-256 elliptic curve in uncompressed form. Must only be called after
+ // validity of the message has been verified.
+ const std::string& public_key() const {
+ CHECK(is_valid_);
+ return public_key_;
+ }
+
+ // Returns the ciphertext for the message. This will be at least the size of
+ // a single record, which is 18 octets. Must only be called after validity of
+ // the message has been verified.
+ const std::string& ciphertext() const {
+ CHECK(is_valid_);
+ return ciphertext_;
+ }
+
+ private:
+ bool is_valid_ = false;
+ absl::optional<GCMDecryptionResult> failure_reason_;
+
+ std::string salt_;
+ uint32_t record_size_ = 0;
+ std::string public_key_;
+ std::string ciphertext_;
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_CRYPTO_MESSAGE_PAYLOAD_PARSER_H_
diff --git a/chromium/components/gcm_driver/crypto/message_payload_parser_unittest.cc b/chromium/components/gcm_driver/crypto/message_payload_parser_unittest.cc
new file mode 100644
index 00000000000..f9d72032a62
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/message_payload_parser_unittest.cc
@@ -0,0 +1,124 @@
+// Copyright 2017 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.
+
+#include "components/gcm_driver/crypto/message_payload_parser.h"
+
+#include "base/big_endian.h"
+#include "base/cxx17_backports.h"
+#include "components/gcm_driver/crypto/gcm_decryption_result.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+
+constexpr size_t kSaltSize = 16;
+constexpr size_t kPublicKeySize = 65;
+constexpr size_t kCiphertextSize = 18;
+
+const uint8_t kValidMessage[] = {
+ // salt (16 bytes, kSaltSize)
+ 0x59, 0xFD, 0x35, 0x97, 0x3B, 0xF3, 0x66, 0xA7, 0xEB, 0x8D, 0x44, 0x1E,
+ 0xCB, 0x4D, 0xFC, 0xD8,
+ // rs (4 bytes, in network byte order)
+ 0x00, 0x00, 0x00, 0x12,
+ // idlen (1 byte)
+ 0x41,
+ // public key (65 bytes, kPublicKeySize, must start with 0x04)
+ 0x04, 0x35, 0x02, 0x67, 0xB9, 0x10, 0x8F, 0x9B, 0xF1, 0x85, 0xF5, 0x1B,
+ 0xD7, 0xA4, 0xEF, 0xBD, 0x28, 0xB3, 0x11, 0x40, 0xBA, 0xD0, 0xEE, 0xB2,
+ 0x97, 0xDA, 0x6A, 0x93, 0x2D, 0x26, 0x45, 0xBD, 0xB2, 0x9A, 0x9F, 0xB8,
+ 0x19, 0xD8, 0x21, 0x6F, 0x66, 0xE3, 0xF6, 0x0B, 0x74, 0xB2, 0x28, 0x38,
+ 0xDC, 0xA7, 0x8A, 0x58, 0x0D, 0x56, 0x47, 0x3E, 0xD0, 0x5B, 0x5C, 0x93,
+ 0x4E, 0xB3, 0x89, 0x87, 0x64,
+ // payload (18 bytes, kCiphertextSize)
+ 0x3F, 0xD8, 0x95, 0x2C, 0xA2, 0x11, 0xBD, 0x7B, 0x57, 0xB2, 0x00, 0xBD,
+ 0x57, 0x68, 0x3F, 0xF0, 0x14, 0x57};
+
+static_assert(base::size(kValidMessage) == 104,
+ "The smallest valid message is 104 bytes in size.");
+
+// Creates an std::string for the |kValidMessage| constant.
+std::string CreateMessageString() {
+ return std::string(reinterpret_cast<const char*>(kValidMessage),
+ base::size(kValidMessage));
+}
+
+TEST(MessagePayloadParserTest, ValidMessage) {
+ MessagePayloadParser parser(CreateMessageString());
+ ASSERT_TRUE(parser.IsValid());
+
+ const uint8_t* salt = kValidMessage;
+
+ ASSERT_EQ(parser.salt().size(), kSaltSize);
+ EXPECT_EQ(parser.salt(), std::string(salt, salt + kSaltSize));
+
+ ASSERT_EQ(parser.record_size(), 18u);
+
+ const uint8_t* public_key =
+ kValidMessage + kSaltSize + sizeof(uint32_t) + sizeof(uint8_t);
+
+ ASSERT_EQ(parser.public_key().size(), kPublicKeySize);
+ EXPECT_EQ(parser.public_key(),
+ std::string(public_key, public_key + kPublicKeySize));
+
+ const uint8_t* ciphertext = kValidMessage + kSaltSize + sizeof(uint32_t) +
+ sizeof(uint8_t) + kPublicKeySize;
+
+ ASSERT_EQ(parser.ciphertext().size(), kCiphertextSize);
+ EXPECT_EQ(parser.ciphertext(),
+ std::string(ciphertext, ciphertext + kCiphertextSize));
+}
+
+TEST(MessagePayloadParserTest, MinimumMessageSize) {
+ std::string message = CreateMessageString();
+ message.resize(base::size(kValidMessage) / 2);
+
+ MessagePayloadParser parser(message);
+ EXPECT_FALSE(parser.IsValid());
+ EXPECT_EQ(parser.GetFailureReason(),
+ GCMDecryptionResult::INVALID_BINARY_HEADER_PAYLOAD_LENGTH);
+}
+
+TEST(MessagePayloadParserTest, MinimumRecordSize) {
+ std::string message = CreateMessageString();
+
+ uint32_t invalid_record_size = 11;
+ base::WriteBigEndian(&message[0] + 16 /* salt */, invalid_record_size);
+
+ MessagePayloadParser parser(message);
+ EXPECT_FALSE(parser.IsValid());
+ EXPECT_EQ(parser.GetFailureReason(),
+ GCMDecryptionResult::INVALID_BINARY_HEADER_RECORD_SIZE);
+}
+
+TEST(MessagePayloadParserTest, InvalidPublicKeyLength) {
+ std::string message = CreateMessageString();
+
+ uint8_t invalid_public_key_size = 42;
+ base::WriteBigEndian(&message[0] + 16 /* salt */ + 4 /* rs */,
+ invalid_public_key_size);
+
+ MessagePayloadParser parser(message);
+ EXPECT_FALSE(parser.IsValid());
+ EXPECT_EQ(parser.GetFailureReason(),
+ GCMDecryptionResult::INVALID_BINARY_HEADER_PUBLIC_KEY_LENGTH);
+}
+
+TEST(MessagePayloadParserTest, InvalidPublicKeyFormat) {
+ std::string message = CreateMessageString();
+
+ uint8_t invalid_p256_uncompressed_key_prefix = 0x42;
+ base::WriteBigEndian(&message[0] + 16 /* salt */ + 4 /* rs */ + 1 /* idlen */,
+ invalid_p256_uncompressed_key_prefix);
+
+ MessagePayloadParser parser(message);
+ EXPECT_FALSE(parser.IsValid());
+ EXPECT_EQ(parser.GetFailureReason(),
+ GCMDecryptionResult::INVALID_BINARY_HEADER_PUBLIC_KEY_FORMAT);
+}
+
+} // namespace
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/crypto/p256_key_util.cc b/chromium/components/gcm_driver/crypto/p256_key_util.cc
new file mode 100644
index 00000000000..5954d20186f
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/p256_key_util.cc
@@ -0,0 +1,95 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/crypto/p256_key_util.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <memory>
+#include <vector>
+
+#include "base/logging.h"
+#include "base/strings/string_util.h"
+#include "crypto/ec_private_key.h"
+#include "third_party/boringssl/src/include/openssl/ec.h"
+#include "third_party/boringssl/src/include/openssl/ecdh.h"
+#include "third_party/boringssl/src/include/openssl/evp.h"
+
+namespace gcm {
+
+namespace {
+
+// A P-256 field element consists of 32 bytes.
+const size_t kFieldBytes = 32;
+
+// A P-256 point in uncompressed form consists of 0x04 (to denote that the point
+// is uncompressed per SEC1 2.3.3) followed by two, 32-byte field elements.
+const size_t kUncompressedPointBytes = 1 + 2 * kFieldBytes;
+
+} // namespace
+
+bool GetRawPublicKey(const crypto::ECPrivateKey& key, std::string* public_key) {
+ DCHECK(public_key);
+ std::string candidate_public_key;
+
+ // ECPrivateKey::ExportRawPublicKey() returns the EC point in the uncompressed
+ // point format.
+ if (!key.ExportRawPublicKey(&candidate_public_key) ||
+ candidate_public_key.size() != kUncompressedPointBytes) {
+ DLOG(ERROR) << "Unable to export the public key.";
+ return false;
+ }
+ public_key->erase();
+ public_key->reserve(kUncompressedPointBytes);
+ public_key->append(candidate_public_key);
+ return true;
+}
+
+// TODO(peter): Get rid of this once all key management code has been updated
+// to use ECPrivateKey instead of std::string.
+bool GetRawPrivateKey(const crypto::ECPrivateKey& key,
+ std::string* private_key) {
+ DCHECK(private_key);
+ std::vector<uint8_t> private_key_vector;
+ if (!key.ExportPrivateKey(&private_key_vector))
+ return false;
+ private_key->assign(private_key_vector.begin(), private_key_vector.end());
+ return true;
+}
+
+bool ComputeSharedP256Secret(crypto::ECPrivateKey& key,
+ const base::StringPiece& peer_public_key,
+ std::string* out_shared_secret) {
+ DCHECK(out_shared_secret);
+
+ EC_KEY* ec_private_key = EVP_PKEY_get0_EC_KEY(key.key());
+ if (!ec_private_key || !EC_KEY_check_key(ec_private_key)) {
+ DLOG(ERROR) << "The private key is invalid.";
+ return false;
+ }
+
+ bssl::UniquePtr<EC_POINT> point(
+ EC_POINT_new(EC_KEY_get0_group(ec_private_key)));
+
+ if (!point || !EC_POINT_oct2point(
+ EC_KEY_get0_group(ec_private_key), point.get(),
+ reinterpret_cast<const uint8_t*>(peer_public_key.data()),
+ peer_public_key.size(), nullptr)) {
+ DLOG(ERROR) << "Can't convert peer public value to curve point.";
+ return false;
+ }
+
+ uint8_t result[kFieldBytes];
+ if (ECDH_compute_key(result, sizeof(result), point.get(), ec_private_key,
+ nullptr) != sizeof(result)) {
+ DLOG(ERROR) << "Unable to compute the ECDH shared secret.";
+ return false;
+ }
+
+ out_shared_secret->assign(reinterpret_cast<char*>(result), sizeof(result));
+ return true;
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/crypto/p256_key_util.h b/chromium/components/gcm_driver/crypto/p256_key_util.h
new file mode 100644
index 00000000000..05bd36c6e9c
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/p256_key_util.h
@@ -0,0 +1,43 @@
+// Copyright 2015 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 COMPONENTS_GCM_DRIVER_CRYPTO_P256_KEY_UTIL_H_
+#define COMPONENTS_GCM_DRIVER_CRYPTO_P256_KEY_UTIL_H_
+
+#include <string>
+
+#include "base/compiler_specific.h"
+#include "base/strings/string_piece.h"
+
+namespace crypto {
+class ECPrivateKey;
+}
+
+namespace gcm {
+
+// Writes the public key associated with the |key| to |*public_key| in
+// uncompressed point format. That is, a 65-octet sequence that starts with a
+// 0x04 octet. Returns whether the public key could be extracted successfully.
+bool GetRawPublicKey(const crypto::ECPrivateKey& key,
+ std::string* public_key) WARN_UNUSED_RESULT;
+
+// Writes the private key associated with the |key| to |*private_key| as a PKCS
+// #8 PrivateKeyInfo block. Returns whether the private key could be extracted
+// successfully.
+bool GetRawPrivateKey(const crypto::ECPrivateKey& key,
+ std::string* private_key) WARN_UNUSED_RESULT;
+
+// Computes the shared secret between |key| and |peer_public_key|.The
+// |peer_public_key| must be an octet string in uncompressed form per
+// SEC1 2.3.3.
+//
+// Returns whether the secret could be computed, and was written to the out
+// argument.
+bool ComputeSharedP256Secret(crypto::ECPrivateKey& key,
+ const base::StringPiece& peer_public_key,
+ std::string* out_shared_secret) WARN_UNUSED_RESULT;
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_CRYPTO_P256_KEY_UTIL_H_
diff --git a/chromium/components/gcm_driver/crypto/p256_key_util_unittest.cc b/chromium/components/gcm_driver/crypto/p256_key_util_unittest.cc
new file mode 100644
index 00000000000..27c01758984
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/p256_key_util_unittest.cc
@@ -0,0 +1,158 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/crypto/p256_key_util.h"
+
+#include <stddef.h>
+
+#include <set>
+
+#include "base/base64.h"
+#include "crypto/ec_private_key.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+
+// A P-256 point in uncompressed form consists of 0x04 (to denote that the point
+// is uncompressed per SEC1 2.3.3) followed by two, 32-byte field elements.
+const size_t kUncompressedPointBytes = 1 + 2 * 32;
+
+// Precomputed private/public key-pair. Keys are stored on disk, so previously
+// created values must continue to be usable for computing shared secrets.
+const char kBobPrivateKey[] =
+ "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgS8wRbDOWz0lKExvIVQiRKtPAP8"
+ "dgHUHAw5gyOd5d4jKhRANCAARZb49Va5MD/KcWtc0oiWc2e8njBDtQzj0mzcOl1fDSt16Pvu6p"
+ "fTU3MTWnImDNnkPxtXm58K7Uax8jFxA4TeXJ";
+const char kBobPublicKey[] =
+ "BFlvj1VrkwP8pxa1zSiJZzZ7yeMEO1DOPSbNw6XV8NK3Xo++7ql9NTcxNaciYM2eQ/G1ebnwrt"
+ "RrHyMXEDhN5ck=";
+
+const char kCarolPrivateKey[] =
+ "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgmqy/ighwCm+RBP4Kct3rzaFEJ"
+ "CZhokknro3KYsriurChRANCAAScr5sTsqmlP8SqiI+8fzxVLr1pby2HyG5mC5J0WSpYVIpMNS"
+ "C16k1qcxqOJ4fiv8Ya47FYw/MIS7X1kobK27mP";
+const char kCarolPublicKey[] =
+ "BJyvmxOyqaU/xKqIj7x/PFUuvWlvLYfIbmYLknRZKlhUikw1ILXqTWpzGo4nh+K/xhrjsVjD8"
+ "whLtfWShsrbuY8=";
+
+// The shared secret between Bob and Carol.
+const char kBobCarolSharedSecret[] =
+ "AUNmKkgLLVLf6j/VnA9Eg1CiPSPfQHGirQj79n4vOyw=";
+
+TEST(P256KeyUtilTest, UniqueKeyPairGeneration) {
+ // Canary for determining that no key repetitions are found in few iterations.
+ std::set<std::string> seen_private_keys;
+ std::set<std::string> seen_public_keys;
+
+ for (int iteration = 0; iteration < 10; ++iteration) {
+ SCOPED_TRACE(iteration);
+
+ std::string private_key, public_key;
+ std::unique_ptr<crypto::ECPrivateKey> key(crypto::ECPrivateKey::Create());
+ ASSERT_TRUE(key);
+ ASSERT_TRUE(GetRawPublicKey(*key, &public_key));
+ ASSERT_TRUE(GetRawPrivateKey(*key, &private_key));
+
+ EXPECT_NE(private_key, public_key);
+ EXPECT_GT(private_key.size(), 0u);
+ EXPECT_EQ(public_key.size(), kUncompressedPointBytes);
+
+ EXPECT_EQ(0u, seen_private_keys.count(private_key));
+ EXPECT_EQ(0u, seen_public_keys.count(public_key));
+
+ seen_private_keys.insert(private_key);
+ seen_public_keys.insert(public_key);
+ }
+}
+
+TEST(P256KeyUtilTest, SharedSecretCalculation) {
+ std::unique_ptr<crypto::ECPrivateKey> bob_key =
+ crypto::ECPrivateKey::Create();
+ std::unique_ptr<crypto::ECPrivateKey> alice_key =
+ crypto::ECPrivateKey::Create();
+
+ std::string alice_public_key, bob_public_key, alice_private_key,
+ bob_private_key;
+ ASSERT_TRUE(GetRawPublicKey(*bob_key, &bob_public_key));
+ ASSERT_TRUE(GetRawPublicKey(*alice_key, &alice_public_key));
+ ASSERT_TRUE(GetRawPrivateKey(*bob_key, &bob_private_key));
+ ASSERT_TRUE(GetRawPrivateKey(*alice_key, &alice_private_key));
+ ASSERT_NE(bob_public_key, alice_public_key);
+ ASSERT_NE(bob_private_key, alice_private_key);
+
+ std::string bob_shared_secret, alice_shared_secret;
+ ASSERT_TRUE(
+ ComputeSharedP256Secret(*bob_key, alice_public_key, &bob_shared_secret));
+ ASSERT_TRUE(ComputeSharedP256Secret(*alice_key, bob_public_key,
+ &alice_shared_secret));
+
+ EXPECT_GT(bob_shared_secret.size(), 0u);
+ EXPECT_EQ(bob_shared_secret, alice_shared_secret);
+
+ std::string unused_shared_secret;
+
+ // Empty and too short peer public values should be considered invalid.
+ ASSERT_FALSE(ComputeSharedP256Secret(*bob_key, "", &unused_shared_secret));
+ ASSERT_FALSE(ComputeSharedP256Secret(*bob_key, bob_public_key.substr(1),
+ &unused_shared_secret));
+}
+
+TEST(P256KeyUtilTest, SharedSecretWithPreExistingKey) {
+ std::string bob_private_key, bob_public_key;
+ ASSERT_TRUE(base::Base64Decode(kBobPrivateKey, &bob_private_key));
+ ASSERT_TRUE(base::Base64Decode(kBobPublicKey, &bob_public_key));
+
+ std::vector<uint8_t> bob_private_key_vec(
+ bob_private_key.begin(), bob_private_key.end());
+ std::unique_ptr<crypto::ECPrivateKey> bob_key =
+ crypto::ECPrivateKey::CreateFromPrivateKeyInfo(bob_private_key_vec);
+ ASSERT_TRUE(bob_key);
+ // First verify against a newly created, ephemeral key-pair.
+ std::unique_ptr<crypto::ECPrivateKey> alice_key(
+ crypto::ECPrivateKey::Create());
+ std::string alice_public_key;
+ ASSERT_TRUE(GetRawPublicKey(*alice_key, &alice_public_key));
+ std::string bob_shared_secret, alice_shared_secret;
+
+ ASSERT_TRUE(ComputeSharedP256Secret(*bob_key, alice_public_key,
+ &bob_shared_secret));
+ ASSERT_TRUE(ComputeSharedP256Secret(*alice_key, bob_public_key,
+ &alice_shared_secret));
+
+ EXPECT_GT(bob_shared_secret.size(), 0u);
+ EXPECT_EQ(bob_shared_secret, alice_shared_secret);
+
+ std::string carol_private_key, carol_public_key;
+ ASSERT_TRUE(base::Base64Decode(kCarolPrivateKey, &carol_private_key));
+ ASSERT_TRUE(base::Base64Decode(kCarolPublicKey, &carol_public_key));
+
+ std::vector<uint8_t> carol_private_key_vec(
+ carol_private_key.begin(), carol_private_key.end());
+ std::unique_ptr<crypto::ECPrivateKey> carol_key =
+ crypto::ECPrivateKey::CreateFromPrivateKeyInfo(carol_private_key_vec);
+ ASSERT_TRUE(carol_key);
+ bob_shared_secret.clear();
+ std::string carol_shared_secret;
+
+ // Then verify against another stored key-pair and shared secret.
+ ASSERT_TRUE(ComputeSharedP256Secret(*bob_key, carol_public_key,
+ &bob_shared_secret));
+ ASSERT_TRUE(ComputeSharedP256Secret(*carol_key, bob_public_key,
+ &carol_shared_secret));
+
+ EXPECT_GT(carol_shared_secret.size(), 0u);
+ EXPECT_EQ(carol_shared_secret, bob_shared_secret);
+
+ std::string bob_carol_shared_secret;
+ ASSERT_TRUE(base::Base64Decode(
+ kBobCarolSharedSecret, &bob_carol_shared_secret));
+
+ EXPECT_EQ(carol_shared_secret, bob_carol_shared_secret);
+}
+
+} // namespace
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/crypto/proto/gcm_encryption_data.proto b/chromium/components/gcm_driver/crypto/proto/gcm_encryption_data.proto
new file mode 100644
index 00000000000..d4ceb230894
--- /dev/null
+++ b/chromium/components/gcm_driver/crypto/proto/gcm_encryption_data.proto
@@ -0,0 +1,58 @@
+// Copyright 2015 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.
+
+syntax = "proto2";
+
+option optimize_for = LITE_RUNTIME;
+
+package gcm;
+
+// Stores a public/private key-pair.
+// Next tag: 5
+message KeyPair {
+ // The type of key used for key agreement. Currently only the ECDH key
+ // agreement scheme is supported, using NIST P-256.
+ enum KeyType {
+ ECDH_P256 = 0;
+ }
+
+ required KeyType type = 1;
+
+ // The private key matching the size requirements of |type|.
+ optional bytes private_key = 2;
+
+ reserved 3; // public_key_x509, now deleted.
+
+ // The public key as an uncompressed EC point according to SEC 2.3.3.
+ optional bytes public_key = 4;
+}
+
+// Stores a vector of public/private key-pairs associated with an app id and
+// optionally the authorized entity of an instance id token.
+//
+// In the current implementation, each (app_id, authorized_entity) pair will
+// have a single encryption key-pair associated with it at most. The message
+// allows for multiple key pairs in case we need to force-cycle all keys,
+// allowing the old keys to remain valid for a period of time enabling the web
+// app to update.
+//
+// Next tag: 6
+message EncryptionData {
+ // The app id to whom this encryption data belongs.
+ required string app_id = 1;
+
+ // The sender id of the instance id token that this encryption data belongs
+ // to. Must not be empty. Must be omitted for non-InstanceID registrations.
+ optional string authorized_entity = 4;
+
+ // DEPRECATED: The actual public/private key-pairs.
+ repeated KeyPair keys = 2;
+
+ // P-256 private key stored as a PKCS #8 PrivateKeyInfo block.
+ optional string private_key = 5;
+
+ // The authentication secret associated with the subscription. Must be a
+ // cryptographically secure number of at least 12 bytes.
+ optional bytes auth_secret = 3;
+}
diff --git a/chromium/components/gcm_driver/fake_gcm_app_handler.cc b/chromium/components/gcm_driver/fake_gcm_app_handler.cc
new file mode 100644
index 00000000000..3eabaa4ed44
--- /dev/null
+++ b/chromium/components/gcm_driver/fake_gcm_app_handler.cc
@@ -0,0 +1,85 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/fake_gcm_app_handler.h"
+
+#include <memory>
+
+#include "base/run_loop.h"
+
+namespace gcm {
+
+FakeGCMAppHandler::FakeGCMAppHandler() : received_event_(NO_EVENT) {}
+
+FakeGCMAppHandler::~FakeGCMAppHandler() = default;
+
+void FakeGCMAppHandler::WaitForNotification() {
+ run_loop_ = std::make_unique<base::RunLoop>();
+ run_loop_->Run();
+ run_loop_.reset();
+}
+
+void FakeGCMAppHandler::ShutdownHandler() {}
+
+void FakeGCMAppHandler::OnStoreReset() {}
+
+void FakeGCMAppHandler::OnMessage(const std::string& app_id,
+ const IncomingMessage& message) {
+ ClearResults();
+ received_event_ = MESSAGE_EVENT;
+ app_id_ = app_id;
+ message_ = message;
+ if (run_loop_)
+ run_loop_->Quit();
+}
+
+void FakeGCMAppHandler::OnMessagesDeleted(const std::string& app_id) {
+ ClearResults();
+ received_event_ = MESSAGES_DELETED_EVENT;
+ app_id_ = app_id;
+ if (run_loop_)
+ run_loop_->Quit();
+}
+
+void FakeGCMAppHandler::OnSendError(
+ const std::string& app_id,
+ const GCMClient::SendErrorDetails& send_error_details) {
+ ClearResults();
+ received_event_ = SEND_ERROR_EVENT;
+ app_id_ = app_id;
+ send_error_details_ = send_error_details;
+ if (run_loop_)
+ run_loop_->Quit();
+}
+
+void FakeGCMAppHandler::OnSendAcknowledged(
+ const std::string& app_id,
+ const std::string& message_id) {
+ ClearResults();
+ app_id_ = app_id;
+ acked_message_id_ = message_id;
+ if (run_loop_)
+ run_loop_->Quit();
+}
+
+void FakeGCMAppHandler::OnMessageDecryptionFailed(
+ const std::string& app_id,
+ const std::string& message_id,
+ const std::string& error_message) {
+ ClearResults();
+ received_event_ = DECRYPTION_FAILED_EVENT;
+ app_id_ = app_id;
+ if (run_loop_)
+ run_loop_->Quit();
+}
+
+void FakeGCMAppHandler::ClearResults() {
+ received_event_ = NO_EVENT;
+ app_id_.clear();
+ acked_message_id_.clear();
+ message_ = IncomingMessage();
+ send_error_details_ = GCMClient::SendErrorDetails();
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/fake_gcm_app_handler.h b/chromium/components/gcm_driver/fake_gcm_app_handler.h
new file mode 100644
index 00000000000..a7c7edde29d
--- /dev/null
+++ b/chromium/components/gcm_driver/fake_gcm_app_handler.h
@@ -0,0 +1,75 @@
+// Copyright 2014 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 COMPONENTS_GCM_DRIVER_FAKE_GCM_APP_HANDLER_H_
+#define COMPONENTS_GCM_DRIVER_FAKE_GCM_APP_HANDLER_H_
+
+#include <memory>
+
+#include "base/compiler_specific.h"
+#include "components/gcm_driver/gcm_app_handler.h"
+
+namespace base {
+class RunLoop;
+}
+
+namespace gcm {
+
+class FakeGCMAppHandler : public GCMAppHandler {
+ public:
+ enum Event {
+ NO_EVENT,
+ MESSAGE_EVENT,
+ MESSAGES_DELETED_EVENT,
+ SEND_ERROR_EVENT,
+ DECRYPTION_FAILED_EVENT,
+ };
+
+ FakeGCMAppHandler();
+
+ FakeGCMAppHandler(const FakeGCMAppHandler&) = delete;
+ FakeGCMAppHandler& operator=(const FakeGCMAppHandler&) = delete;
+
+ ~FakeGCMAppHandler() override;
+
+ const Event& received_event() const { return received_event_; }
+ const std::string& app_id() const { return app_id_; }
+ const std::string& acked_message_id() const { return acked_message_id_; }
+ const IncomingMessage& message() const { return message_; }
+ const GCMClient::SendErrorDetails& send_error_details() const {
+ return send_error_details_;
+ }
+
+ void WaitForNotification();
+
+ // GCMAppHandler implementation.
+ void ShutdownHandler() override;
+ void OnStoreReset() override;
+ void OnMessage(const std::string& app_id,
+ const IncomingMessage& message) override;
+ void OnMessagesDeleted(const std::string& app_id) override;
+ void OnSendError(
+ const std::string& app_id,
+ const GCMClient::SendErrorDetails& send_error_details) override;
+ void OnMessageDecryptionFailed(const std::string& app_id,
+ const std::string& message_id,
+ const std::string& error_message) override;
+ void OnSendAcknowledged(const std::string& app_id,
+ const std::string& message_id) override;
+
+ private:
+ void ClearResults();
+
+ std::unique_ptr<base::RunLoop> run_loop_;
+
+ Event received_event_;
+ std::string app_id_;
+ std::string acked_message_id_;
+ IncomingMessage message_;
+ GCMClient::SendErrorDetails send_error_details_;
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_FAKE_GCM_APP_HANDLER_H_
diff --git a/chromium/components/gcm_driver/fake_gcm_client.cc b/chromium/components/gcm_driver/fake_gcm_client.cc
new file mode 100644
index 00000000000..69d6d8693a6
--- /dev/null
+++ b/chromium/components/gcm_driver/fake_gcm_client.cc
@@ -0,0 +1,335 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/fake_gcm_client.h"
+
+#include <stddef.h>
+
+#include <algorithm>
+
+#include "base/bind.h"
+#include "base/check.h"
+#include "base/location.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/sys_byteorder.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/task/single_thread_task_runner.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/time/time.h"
+#include "base/timer/timer.h"
+#include "google_apis/gcm/base/encryptor.h"
+#include "google_apis/gcm/engine/account_mapping.h"
+#include "net/base/ip_endpoint.h"
+
+namespace gcm {
+
+// static
+std::string FakeGCMClient::GenerateGCMRegistrationID(
+ const std::vector<std::string>& sender_ids) {
+ // GCMService normalizes the sender IDs by making them sorted.
+ std::vector<std::string> normalized_sender_ids = sender_ids;
+ std::sort(normalized_sender_ids.begin(), normalized_sender_ids.end());
+
+ // Simulate the registration_id by concaternating all sender IDs.
+ // Set registration_id to empty to denote an error if sender_ids contains a
+ // hint.
+ std::string registration_id;
+ if (sender_ids.size() != 1 ||
+ sender_ids[0].find("error") == std::string::npos) {
+ for (size_t i = 0; i < normalized_sender_ids.size(); ++i) {
+ if (i > 0)
+ registration_id += ",";
+ registration_id += normalized_sender_ids[i];
+ }
+ }
+ return registration_id;
+}
+
+// static
+std::string FakeGCMClient::GenerateInstanceIDToken(
+ const std::string& authorized_entity, const std::string& scope) {
+ if (authorized_entity.find("error") != std::string::npos)
+ return "";
+ std::string token(authorized_entity);
+ token += ",";
+ token += scope;
+ return token;
+}
+
+FakeGCMClient::FakeGCMClient(
+ const scoped_refptr<base::SequencedTaskRunner>& ui_thread,
+ const scoped_refptr<base::SequencedTaskRunner>& io_thread)
+ : delegate_(nullptr),
+ started_(false),
+ start_mode_(DELAYED_START),
+ start_mode_overridding_(RESPECT_START_MODE),
+ ui_thread_(ui_thread),
+ io_thread_(io_thread) {}
+
+FakeGCMClient::~FakeGCMClient() {
+}
+
+void FakeGCMClient::Initialize(
+ const ChromeBuildInfo& chrome_build_info,
+ const base::FilePath& store_path,
+ bool remove_account_mappings_with_email_key,
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner,
+ base::RepeatingCallback<void(
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)>
+ get_socket_factory_callback,
+ const scoped_refptr<network::SharedURLLoaderFactory>& url_loader_factory,
+ network::NetworkConnectionTracker* network_connection_tracker,
+ std::unique_ptr<Encryptor> encryptor,
+ Delegate* delegate) {
+ product_category_for_subtypes_ =
+ chrome_build_info.product_category_for_subtypes;
+ delegate_ = delegate;
+}
+
+void FakeGCMClient::Start(StartMode start_mode) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ if (started_)
+ return;
+
+ if (start_mode == IMMEDIATE_START)
+ start_mode_ = IMMEDIATE_START;
+ if (start_mode_ == DELAYED_START ||
+ start_mode_overridding_ == FORCE_TO_ALWAYS_DELAY_START_GCM) {
+ return;
+ }
+
+ DoStart();
+}
+
+void FakeGCMClient::DoStart() {
+ started_ = true;
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&FakeGCMClient::Started, weak_ptr_factory_.GetWeakPtr()));
+}
+
+void FakeGCMClient::Stop() {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+ started_ = false;
+ delegate_->OnDisconnected();
+}
+
+void FakeGCMClient::Register(
+ scoped_refptr<RegistrationInfo> registration_info) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ std::string registration_id;
+
+ GCMRegistrationInfo* gcm_registration_info =
+ GCMRegistrationInfo::FromRegistrationInfo(registration_info.get());
+ if (gcm_registration_info) {
+ registration_id = GenerateGCMRegistrationID(
+ gcm_registration_info->sender_ids);
+ }
+
+ InstanceIDTokenInfo* instance_id_token_info =
+ InstanceIDTokenInfo::FromRegistrationInfo(registration_info.get());
+ if (instance_id_token_info) {
+ registration_id = GenerateInstanceIDToken(
+ instance_id_token_info->authorized_entity,
+ instance_id_token_info->scope);
+ }
+
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::BindOnce(&FakeGCMClient::RegisterFinished,
+ weak_ptr_factory_.GetWeakPtr(),
+ std::move(registration_info), registration_id));
+}
+
+bool FakeGCMClient::ValidateRegistration(
+ scoped_refptr<RegistrationInfo> registration_info,
+ const std::string& registration_id) {
+ return true;
+}
+
+void FakeGCMClient::Unregister(
+ scoped_refptr<RegistrationInfo> registration_info) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&FakeGCMClient::UnregisterFinished,
+ weak_ptr_factory_.GetWeakPtr(), registration_info));
+}
+
+void FakeGCMClient::Send(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&FakeGCMClient::SendFinished,
+ weak_ptr_factory_.GetWeakPtr(), app_id, message));
+}
+
+void FakeGCMClient::RecordDecryptionFailure(const std::string& app_id,
+ GCMDecryptionResult result) {
+ recorder_.RecordDecryptionFailure(app_id, result);
+}
+
+void FakeGCMClient::SetRecording(bool recording) {
+ recorder_.set_is_recording(recording);
+}
+
+void FakeGCMClient::ClearActivityLogs() {
+ recorder_.Clear();
+}
+
+GCMClient::GCMStatistics FakeGCMClient::GetStatistics() const {
+ GCMClient::GCMStatistics statistics;
+ statistics.is_recording = recorder_.is_recording();
+
+ recorder_.CollectActivities(&statistics.recorded_activities);
+ return statistics;
+}
+
+void FakeGCMClient::SetAccountTokens(
+ const std::vector<AccountTokenInfo>& account_tokens) {
+}
+
+void FakeGCMClient::UpdateAccountMapping(
+ const AccountMapping& account_mapping) {
+}
+
+void FakeGCMClient::RemoveAccountMapping(const CoreAccountId& account_id) {}
+
+void FakeGCMClient::SetLastTokenFetchTime(const base::Time& time) {
+}
+
+void FakeGCMClient::UpdateHeartbeatTimer(
+ std::unique_ptr<base::RetainingOneShotTimer> timer) {}
+
+void FakeGCMClient::AddInstanceIDData(const std::string& app_id,
+ const std::string& instance_id,
+ const std::string& extra_data) {
+ instance_id_data_[app_id] = make_pair(instance_id, extra_data);
+}
+
+void FakeGCMClient::RemoveInstanceIDData(const std::string& app_id) {
+ instance_id_data_.erase(app_id);
+}
+
+void FakeGCMClient::GetInstanceIDData(const std::string& app_id,
+ std::string* instance_id,
+ std::string* extra_data) {
+ auto iter = instance_id_data_.find(app_id);
+ if (iter == instance_id_data_.end()) {
+ instance_id->clear();
+ extra_data->clear();
+ return;
+ }
+
+ *instance_id = iter->second.first;
+ *extra_data = iter->second.second;
+}
+
+void FakeGCMClient::AddHeartbeatInterval(const std::string& scope,
+ int interval_ms) {
+}
+
+void FakeGCMClient::RemoveHeartbeatInterval(const std::string& scope) {
+}
+
+void FakeGCMClient::PerformDelayedStart() {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ io_thread_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&FakeGCMClient::DoStart, weak_ptr_factory_.GetWeakPtr()));
+}
+
+void FakeGCMClient::ReceiveMessage(const std::string& app_id,
+ const IncomingMessage& message) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ io_thread_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&FakeGCMClient::MessageReceived,
+ weak_ptr_factory_.GetWeakPtr(), app_id, message));
+}
+
+void FakeGCMClient::DeleteMessages(const std::string& app_id) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ io_thread_->PostTask(FROM_HERE,
+ base::BindOnce(&FakeGCMClient::MessagesDeleted,
+ weak_ptr_factory_.GetWeakPtr(), app_id));
+}
+
+void FakeGCMClient::Started() {
+ delegate_->OnGCMReady(std::vector<AccountMapping>(), base::Time());
+ delegate_->OnConnected(net::IPEndPoint());
+}
+
+void FakeGCMClient::RegisterFinished(
+ scoped_refptr<RegistrationInfo> registration_info,
+ const std::string& registrion_id) {
+ delegate_->OnRegisterFinished(std::move(registration_info), registrion_id,
+ registrion_id.empty() ? SERVER_ERROR : SUCCESS);
+}
+
+void FakeGCMClient::UnregisterFinished(
+ scoped_refptr<RegistrationInfo> registration_info) {
+ delegate_->OnUnregisterFinished(std::move(registration_info),
+ GCMClient::SUCCESS);
+}
+
+void FakeGCMClient::SendFinished(const std::string& app_id,
+ const OutgoingMessage& message) {
+ delegate_->OnSendFinished(app_id, message.id, SUCCESS);
+
+ // Simulate send error if message id contains a hint.
+ if (message.id.find("error") != std::string::npos) {
+ SendErrorDetails send_error_details;
+ send_error_details.message_id = message.id;
+ send_error_details.result = NETWORK_ERROR;
+ send_error_details.additional_data = message.data;
+ base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
+ FROM_HERE,
+ base::BindOnce(&FakeGCMClient::MessageSendError,
+ weak_ptr_factory_.GetWeakPtr(), app_id,
+ send_error_details),
+ base::Milliseconds(200));
+ } else if(message.id.find("ack") != std::string::npos) {
+ base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
+ FROM_HERE,
+ base::BindOnce(&FakeGCMClient::SendAcknowledgement,
+ weak_ptr_factory_.GetWeakPtr(), app_id, message.id),
+ base::Milliseconds(200));
+ }
+}
+
+void FakeGCMClient::MessageReceived(const std::string& app_id,
+ const IncomingMessage& message) {
+ if (delegate_)
+ delegate_->OnMessageReceived(app_id, message);
+}
+
+void FakeGCMClient::MessagesDeleted(const std::string& app_id) {
+ if (delegate_)
+ delegate_->OnMessagesDeleted(app_id);
+}
+
+void FakeGCMClient::MessageSendError(
+ const std::string& app_id,
+ const GCMClient::SendErrorDetails& send_error_details) {
+ if (delegate_)
+ delegate_->OnMessageSendError(app_id, send_error_details);
+}
+
+void FakeGCMClient::SendAcknowledgement(const std::string& app_id,
+ const std::string& message_id) {
+ if (delegate_)
+ delegate_->OnSendAcknowledged(app_id, message_id);
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/fake_gcm_client.h b/chromium/components/gcm_driver/fake_gcm_client.h
new file mode 100644
index 00000000000..dcc50751400
--- /dev/null
+++ b/chromium/components/gcm_driver/fake_gcm_client.h
@@ -0,0 +1,139 @@
+// Copyright 2014 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 COMPONENTS_GCM_DRIVER_FAKE_GCM_CLIENT_H_
+#define COMPONENTS_GCM_DRIVER_FAKE_GCM_CLIENT_H_
+
+#include <map>
+
+#include "base/compiler_specific.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/timer/timer.h"
+#include "components/gcm_driver/gcm_client.h"
+#include "components/gcm_driver/gcm_stats_recorder_impl.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+
+namespace base {
+class SequencedTaskRunner;
+}
+
+namespace gcm {
+
+class FakeGCMClient : public GCMClient {
+ public:
+ // For testing purpose.
+ enum StartModeOverridding {
+ // No change to how delay start is handled.
+ RESPECT_START_MODE,
+ // Force to delay start GCM until PerformDelayedStart is called.
+ FORCE_TO_ALWAYS_DELAY_START_GCM,
+ };
+
+ // Generate and return the registration ID/token based on parameters for
+ // testing verification.
+ static std::string GenerateGCMRegistrationID(
+ const std::vector<std::string>& sender_ids);
+ static std::string GenerateInstanceIDToken(
+ const std::string& authorized_entity, const std::string& scope);
+
+ FakeGCMClient(const scoped_refptr<base::SequencedTaskRunner>& ui_thread,
+ const scoped_refptr<base::SequencedTaskRunner>& io_thread);
+
+ FakeGCMClient(const FakeGCMClient&) = delete;
+ FakeGCMClient& operator=(const FakeGCMClient&) = delete;
+
+ ~FakeGCMClient() override;
+
+ // Overridden from GCMClient:
+ // Called on IO thread.
+ void Initialize(
+ const ChromeBuildInfo& chrome_build_info,
+ const base::FilePath& store_path,
+ bool remove_account_mappings_with_email_key,
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner,
+ base::RepeatingCallback<void(
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)>
+ get_socket_factory_callback,
+ const scoped_refptr<network::SharedURLLoaderFactory>& url_loader_factory,
+ network::NetworkConnectionTracker* network_connection_tracker,
+ std::unique_ptr<Encryptor> encryptor,
+ Delegate* delegate) override;
+ void Start(StartMode start_mode) override;
+ void Stop() override;
+ void Register(scoped_refptr<RegistrationInfo> registration_info) override;
+ bool ValidateRegistration(scoped_refptr<RegistrationInfo> registration_info,
+ const std::string& registration_id) override;
+ void Unregister(scoped_refptr<RegistrationInfo> registration_info) override;
+ void Send(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) override;
+ void RecordDecryptionFailure(const std::string& app_id,
+ GCMDecryptionResult result) override;
+ void SetRecording(bool recording) override;
+ void ClearActivityLogs() override;
+ GCMStatistics GetStatistics() const override;
+ void SetAccountTokens(
+ const std::vector<AccountTokenInfo>& account_tokens) override;
+ void UpdateAccountMapping(const AccountMapping& account_mapping) override;
+ void RemoveAccountMapping(const CoreAccountId& account_id) override;
+ void SetLastTokenFetchTime(const base::Time& time) override;
+ void UpdateHeartbeatTimer(
+ std::unique_ptr<base::RetainingOneShotTimer> timer) override;
+ void AddInstanceIDData(const std::string& app_id,
+ const std::string& instance_id,
+ const std::string& extra_data) override;
+ void RemoveInstanceIDData(const std::string& app_id) override;
+ void GetInstanceIDData(const std::string& app_id,
+ std::string* instance_id,
+ std::string* extra_data) override;
+ void AddHeartbeatInterval(const std::string& scope, int interval_ms) override;
+ void RemoveHeartbeatInterval(const std::string& scope) override;
+
+ // Initiate the start that has been delayed.
+ // Called on UI thread.
+ void PerformDelayedStart();
+
+ // Simulate receiving something from the server.
+ // Called on UI thread.
+ void ReceiveMessage(const std::string& app_id,
+ const IncomingMessage& message);
+ void DeleteMessages(const std::string& app_id);
+
+ void set_start_mode_overridding(StartModeOverridding overridding) {
+ start_mode_overridding_ = overridding;
+ }
+
+ private:
+ // Called on IO thread.
+ void DoStart();
+ void Started();
+ void RegisterFinished(scoped_refptr<RegistrationInfo> registration_info,
+ const std::string& registrion_id);
+ void UnregisterFinished(scoped_refptr<RegistrationInfo> registration_info);
+ void SendFinished(const std::string& app_id, const OutgoingMessage& message);
+ void MessageReceived(const std::string& app_id,
+ const IncomingMessage& message);
+ void MessagesDeleted(const std::string& app_id);
+ void MessageSendError(const std::string& app_id,
+ const SendErrorDetails& send_error_details);
+ void SendAcknowledgement(const std::string& app_id,
+ const std::string& message_id);
+
+ raw_ptr<Delegate> delegate_;
+ std::string product_category_for_subtypes_;
+ bool started_;
+ StartMode start_mode_;
+ StartModeOverridding start_mode_overridding_;
+ scoped_refptr<base::SequencedTaskRunner> ui_thread_;
+ scoped_refptr<base::SequencedTaskRunner> io_thread_;
+ std::map<std::string, std::pair<std::string, std::string>> instance_id_data_;
+ GCMStatsRecorderImpl recorder_;
+ base::WeakPtrFactory<FakeGCMClient> weak_ptr_factory_{this};
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_FAKE_GCM_CLIENT_H_
diff --git a/chromium/components/gcm_driver/fake_gcm_client_factory.cc b/chromium/components/gcm_driver/fake_gcm_client_factory.cc
new file mode 100644
index 00000000000..78f6ff1cd06
--- /dev/null
+++ b/chromium/components/gcm_driver/fake_gcm_client_factory.cc
@@ -0,0 +1,28 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/fake_gcm_client_factory.h"
+
+#include <memory>
+
+#include "base/task/sequenced_task_runner.h"
+#include "components/gcm_driver/gcm_client.h"
+
+namespace gcm {
+
+FakeGCMClientFactory::FakeGCMClientFactory(
+ const scoped_refptr<base::SequencedTaskRunner>& ui_thread,
+ const scoped_refptr<base::SequencedTaskRunner>& io_thread)
+ : ui_thread_(ui_thread),
+ io_thread_(io_thread) {
+}
+
+FakeGCMClientFactory::~FakeGCMClientFactory() {
+}
+
+std::unique_ptr<GCMClient> FakeGCMClientFactory::BuildInstance() {
+ return std::unique_ptr<GCMClient>(new FakeGCMClient(ui_thread_, io_thread_));
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/fake_gcm_client_factory.h b/chromium/components/gcm_driver/fake_gcm_client_factory.h
new file mode 100644
index 00000000000..a20c990f87c
--- /dev/null
+++ b/chromium/components/gcm_driver/fake_gcm_client_factory.h
@@ -0,0 +1,41 @@
+// Copyright 2014 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 COMPONENTS_GCM_DRIVER_FAKE_GCM_CLIENT_FACTORY_H_
+#define COMPONENTS_GCM_DRIVER_FAKE_GCM_CLIENT_FACTORY_H_
+
+#include "base/compiler_specific.h"
+#include "components/gcm_driver/fake_gcm_client.h"
+#include "components/gcm_driver/gcm_client_factory.h"
+
+namespace base {
+class SequencedTaskRunner;
+}
+
+namespace gcm {
+
+class GCMClient;
+
+class FakeGCMClientFactory : public GCMClientFactory {
+ public:
+ FakeGCMClientFactory(
+ const scoped_refptr<base::SequencedTaskRunner>& ui_thread,
+ const scoped_refptr<base::SequencedTaskRunner>& io_thread);
+
+ FakeGCMClientFactory(const FakeGCMClientFactory&) = delete;
+ FakeGCMClientFactory& operator=(const FakeGCMClientFactory&) = delete;
+
+ ~FakeGCMClientFactory() override;
+
+ // GCMClientFactory:
+ std::unique_ptr<GCMClient> BuildInstance() override;
+
+ private:
+ scoped_refptr<base::SequencedTaskRunner> ui_thread_;
+ scoped_refptr<base::SequencedTaskRunner> io_thread_;
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_FAKE_GCM_CLIENT_FACTORY_H_
diff --git a/chromium/components/gcm_driver/fake_gcm_driver.cc b/chromium/components/gcm_driver/fake_gcm_driver.cc
new file mode 100644
index 00000000000..81f6318ab63
--- /dev/null
+++ b/chromium/components/gcm_driver/fake_gcm_driver.cc
@@ -0,0 +1,110 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/fake_gcm_driver.h"
+
+#include "base/bind.h"
+#include "base/files/file_path.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/threading/thread_task_runner_handle.h"
+
+namespace gcm {
+
+FakeGCMDriver::FakeGCMDriver() : GCMDriver(base::FilePath(), nullptr) {}
+
+FakeGCMDriver::FakeGCMDriver(
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner)
+ : GCMDriver(base::FilePath(), blocking_task_runner) {}
+
+FakeGCMDriver::~FakeGCMDriver() = default;
+
+void FakeGCMDriver::ValidateRegistration(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ const std::string& registration_id,
+ ValidateRegistrationCallback callback) {
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::BindOnce(std::move(callback), true /* is_valid */));
+}
+
+void FakeGCMDriver::OnSignedIn() {
+}
+
+void FakeGCMDriver::OnSignedOut() {
+}
+
+void FakeGCMDriver::AddConnectionObserver(GCMConnectionObserver* observer) {
+}
+
+void FakeGCMDriver::RemoveConnectionObserver(GCMConnectionObserver* observer) {
+}
+
+GCMClient* FakeGCMDriver::GetGCMClientForTesting() const {
+ return nullptr;
+}
+
+bool FakeGCMDriver::IsStarted() const {
+ return true;
+}
+
+bool FakeGCMDriver::IsConnected() const {
+ return true;
+}
+
+void FakeGCMDriver::GetGCMStatistics(GetGCMStatisticsCallback callback,
+ ClearActivityLogs clear_logs) {}
+
+void FakeGCMDriver::SetGCMRecording(
+ const GCMStatisticsRecordingCallback& callback,
+ bool recording) {}
+
+GCMClient::Result FakeGCMDriver::EnsureStarted(
+ GCMClient::StartMode start_mode) {
+ return GCMClient::SUCCESS;
+}
+
+void FakeGCMDriver::RegisterImpl(const std::string& app_id,
+ const std::vector<std::string>& sender_ids) {
+}
+
+void FakeGCMDriver::UnregisterImpl(const std::string& app_id) {
+}
+
+void FakeGCMDriver::SendImpl(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) {
+}
+
+void FakeGCMDriver::RecordDecryptionFailure(const std::string& app_id,
+ GCMDecryptionResult result) {}
+
+void FakeGCMDriver::SetAccountTokens(
+ const std::vector<GCMClient::AccountTokenInfo>& account_tokens) {
+}
+
+void FakeGCMDriver::UpdateAccountMapping(
+ const AccountMapping& account_mapping) {
+}
+
+void FakeGCMDriver::RemoveAccountMapping(const CoreAccountId& account_id) {}
+
+base::Time FakeGCMDriver::GetLastTokenFetchTime() {
+ return base::Time();
+}
+
+void FakeGCMDriver::SetLastTokenFetchTime(const base::Time& time) {
+}
+
+InstanceIDHandler* FakeGCMDriver::GetInstanceIDHandlerInternal() {
+ return nullptr;
+}
+
+void FakeGCMDriver::AddHeartbeatInterval(const std::string& scope,
+ int interval_ms) {
+}
+
+void FakeGCMDriver::RemoveHeartbeatInterval(const std::string& scope) {
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/fake_gcm_driver.h b/chromium/components/gcm_driver/fake_gcm_driver.h
new file mode 100644
index 00000000000..91c1701b7da
--- /dev/null
+++ b/chromium/components/gcm_driver/fake_gcm_driver.h
@@ -0,0 +1,71 @@
+// Copyright 2014 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 COMPONENTS_GCM_DRIVER_FAKE_GCM_DRIVER_H_
+#define COMPONENTS_GCM_DRIVER_FAKE_GCM_DRIVER_H_
+
+#include "base/compiler_specific.h"
+#include "base/memory/ref_counted.h"
+#include "components/gcm_driver/gcm_driver.h"
+
+namespace base {
+class SequencedTaskRunner;
+}
+
+namespace gcm {
+
+class FakeGCMDriver : public GCMDriver {
+ public:
+ FakeGCMDriver();
+ explicit FakeGCMDriver(
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner);
+
+ FakeGCMDriver(const FakeGCMDriver&) = delete;
+ FakeGCMDriver& operator=(const FakeGCMDriver&) = delete;
+
+ ~FakeGCMDriver() override;
+
+ // GCMDriver overrides:
+ void ValidateRegistration(const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ const std::string& registration_id,
+ ValidateRegistrationCallback callback) override;
+ void OnSignedIn() override;
+ void OnSignedOut() override;
+ void AddConnectionObserver(GCMConnectionObserver* observer) override;
+ void RemoveConnectionObserver(GCMConnectionObserver* observer) override;
+ GCMClient* GetGCMClientForTesting() const override;
+ bool IsStarted() const override;
+ bool IsConnected() const override;
+ void GetGCMStatistics(GetGCMStatisticsCallback callback,
+ ClearActivityLogs clear_logs) override;
+ void SetGCMRecording(const GCMStatisticsRecordingCallback& callback,
+ bool recording) override;
+ void SetAccountTokens(
+ const std::vector<GCMClient::AccountTokenInfo>& account_tokens) override;
+ void UpdateAccountMapping(const AccountMapping& account_mapping) override;
+ void RemoveAccountMapping(const CoreAccountId& account_id) override;
+ base::Time GetLastTokenFetchTime() override;
+ void SetLastTokenFetchTime(const base::Time& time) override;
+ InstanceIDHandler* GetInstanceIDHandlerInternal() override;
+ void AddHeartbeatInterval(const std::string& scope, int interval_ms) override;
+ void RemoveHeartbeatInterval(const std::string& scope) override;
+
+ protected:
+ // GCMDriver implementation:
+ GCMClient::Result EnsureStarted(
+ GCMClient::StartMode start_mode) override;
+ void RegisterImpl(const std::string& app_id,
+ const std::vector<std::string>& sender_ids) override;
+ void UnregisterImpl(const std::string& app_id) override;
+ void SendImpl(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) override;
+ void RecordDecryptionFailure(const std::string& app_id,
+ GCMDecryptionResult result) override;
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_FAKE_GCM_DRIVER_H_
diff --git a/chromium/components/gcm_driver/fake_gcm_profile_service.cc b/chromium/components/gcm_driver/fake_gcm_profile_service.cc
new file mode 100644
index 00000000000..5d9c0354d3c
--- /dev/null
+++ b/chromium/components/gcm_driver/fake_gcm_profile_service.cc
@@ -0,0 +1,246 @@
+// Copyright 2013 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.
+
+#include "components/gcm_driver/fake_gcm_profile_service.h"
+
+#include <utility>
+
+#include "base/bind.h"
+#include "base/format_macros.h"
+#include "base/location.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
+#include "base/task/single_thread_task_runner.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/time/time.h"
+#include "components/gcm_driver/crypto/gcm_encryption_result.h"
+#include "components/gcm_driver/fake_gcm_client_factory.h"
+#include "components/gcm_driver/gcm_driver.h"
+#include "components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.h"
+
+namespace gcm {
+
+class FakeGCMProfileService::CustomFakeGCMDriver
+ : public instance_id::FakeGCMDriverForInstanceID {
+ public:
+ explicit CustomFakeGCMDriver(FakeGCMProfileService* service);
+
+ CustomFakeGCMDriver(const CustomFakeGCMDriver&) = delete;
+ CustomFakeGCMDriver& operator=(const CustomFakeGCMDriver&) = delete;
+
+ ~CustomFakeGCMDriver() override;
+
+ void OnRegisterFinished(const std::string& app_id,
+ const std::string& registration_id,
+ GCMClient::Result result);
+ void OnSendFinished(const std::string& app_id,
+ const std::string& message_id,
+ GCMClient::Result result);
+
+ void OnDispatchMessage(const std::string& app_id,
+ const IncomingMessage& message);
+
+ // GCMDriver overrides:
+ void EncryptMessage(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& p256dh,
+ const std::string& auth_secret,
+ const std::string& message,
+ EncryptMessageCallback callback) override;
+
+ protected:
+ // FakeGCMDriver overrides:
+ void RegisterImpl(const std::string& app_id,
+ const std::vector<std::string>& sender_ids) override;
+ void UnregisterImpl(const std::string& app_id) override;
+ void UnregisterWithSenderIdImpl(const std::string& app_id,
+ const std::string& sender_id) override;
+ void SendImpl(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) override;
+
+ // FakeGCMDriverForInstanceID overrides:
+ void GetToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ base::TimeDelta time_to_live,
+ GetTokenCallback callback) override;
+ void DeleteToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ DeleteTokenCallback callback) override;
+
+ private:
+ void DoRegister(const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ const std::string& registration_id);
+ void DoSend(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message);
+
+ raw_ptr<FakeGCMProfileService> service_;
+
+ // Used to give each registration a unique registration id. Does not decrease
+ // when unregister is called.
+ int registration_count_ = 0;
+
+ base::WeakPtrFactory<CustomFakeGCMDriver> weak_factory_{
+ this}; // Must be last.
+};
+
+FakeGCMProfileService::CustomFakeGCMDriver::CustomFakeGCMDriver(
+ FakeGCMProfileService* service)
+ : instance_id::FakeGCMDriverForInstanceID(
+ base::ThreadTaskRunnerHandle::Get()),
+ service_(service) {}
+
+FakeGCMProfileService::CustomFakeGCMDriver::~CustomFakeGCMDriver() {}
+
+void FakeGCMProfileService::CustomFakeGCMDriver::RegisterImpl(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids) {
+ if (service_->is_offline_)
+ return; // Drop request.
+
+ // Generate fake registration IDs, encoding the number of sender IDs (used by
+ // GcmApiTest.RegisterValidation), then an incrementing count (even for the
+ // same app_id - there's no caching) so tests can distinguish registrations.
+ std::string registration_id = base::StringPrintf(
+ "%" PRIuS "-%d", sender_ids.size(), registration_count_);
+ ++registration_count_;
+
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::BindOnce(&CustomFakeGCMDriver::DoRegister,
+ weak_factory_.GetWeakPtr(), app_id, sender_ids,
+ registration_id));
+}
+
+void FakeGCMProfileService::CustomFakeGCMDriver::DoRegister(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ const std::string& registration_id) {
+ if (service_->collect_) {
+ service_->last_registered_app_id_ = app_id;
+ service_->last_registered_sender_ids_ = sender_ids;
+ }
+ RegisterFinished(app_id, registration_id, GCMClient::SUCCESS);
+}
+
+void FakeGCMProfileService::CustomFakeGCMDriver::UnregisterImpl(
+ const std::string& app_id) {
+ if (service_->is_offline_)
+ return; // Drop request.
+
+ GCMClient::Result result = GCMClient::SUCCESS;
+ if (!service_->unregister_responses_.empty()) {
+ result = service_->unregister_responses_.front();
+ service_->unregister_responses_.pop_front();
+ }
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::BindOnce(&CustomFakeGCMDriver::UnregisterFinished,
+ weak_factory_.GetWeakPtr(), app_id, result));
+}
+
+void FakeGCMProfileService::CustomFakeGCMDriver::UnregisterWithSenderIdImpl(
+ const std::string& app_id,
+ const std::string& sender_id) {
+ NOTREACHED() << "This Android-specific method is not yet faked.";
+}
+
+void FakeGCMProfileService::CustomFakeGCMDriver::SendImpl(
+ const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) {
+ if (service_->is_offline_)
+ return; // Drop request.
+
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&CustomFakeGCMDriver::DoSend, weak_factory_.GetWeakPtr(),
+ app_id, receiver_id, message));
+}
+
+void FakeGCMProfileService::CustomFakeGCMDriver::EncryptMessage(
+ const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& p256dh,
+ const std::string& auth_secret,
+ const std::string& message,
+ EncryptMessageCallback callback) {
+ // Pretend that message has been encrypted.
+ std::move(callback).Run(GCMEncryptionResult::ENCRYPTED_DRAFT_08, message);
+}
+
+void FakeGCMProfileService::CustomFakeGCMDriver::DoSend(
+ const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) {
+ if (service_->collect_) {
+ service_->last_sent_message_ = message;
+ service_->last_receiver_id_ = receiver_id;
+ }
+ SendFinished(app_id, message.id, GCMClient::SUCCESS);
+}
+
+void FakeGCMProfileService::CustomFakeGCMDriver::GetToken(
+ const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ base::TimeDelta time_to_live,
+ GetTokenCallback callback) {
+ if (service_->is_offline_)
+ return; // Drop request.
+
+ instance_id::FakeGCMDriverForInstanceID::GetToken(
+ app_id, authorized_entity, scope, time_to_live, std::move(callback));
+}
+
+void FakeGCMProfileService::CustomFakeGCMDriver::DeleteToken(
+ const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ DeleteTokenCallback callback) {
+ if (service_->is_offline_)
+ return; // Drop request.
+
+ instance_id::FakeGCMDriverForInstanceID::DeleteToken(
+ app_id, authorized_entity, scope, std::move(callback));
+}
+
+void FakeGCMProfileService::CustomFakeGCMDriver::OnDispatchMessage(
+ const std::string& app_id,
+ const IncomingMessage& message) {
+ DispatchMessage(app_id, message);
+}
+
+// static
+std::unique_ptr<KeyedService> FakeGCMProfileService::Build(
+ content::BrowserContext* context) {
+ std::unique_ptr<FakeGCMProfileService> service =
+ std::make_unique<FakeGCMProfileService>();
+ service->SetDriverForTesting(
+ std::make_unique<CustomFakeGCMDriver>(service.get()));
+
+ return service;
+}
+
+FakeGCMProfileService::FakeGCMProfileService() = default;
+
+FakeGCMProfileService::~FakeGCMProfileService() = default;
+
+void FakeGCMProfileService::AddExpectedUnregisterResponse(
+ GCMClient::Result result) {
+ unregister_responses_.push_back(result);
+}
+
+void FakeGCMProfileService::DispatchMessage(const std::string& app_id,
+ const IncomingMessage& message) {
+ CustomFakeGCMDriver* custom_driver =
+ static_cast<CustomFakeGCMDriver*>(driver());
+ custom_driver->OnDispatchMessage(app_id, message);
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/fake_gcm_profile_service.h b/chromium/components/gcm_driver/fake_gcm_profile_service.h
new file mode 100644
index 00000000000..14f5ff32240
--- /dev/null
+++ b/chromium/components/gcm_driver/fake_gcm_profile_service.h
@@ -0,0 +1,79 @@
+// Copyright 2013 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 COMPONENTS_GCM_DRIVER_FAKE_GCM_PROFILE_SERVICE_H_
+#define COMPONENTS_GCM_DRIVER_FAKE_GCM_PROFILE_SERVICE_H_
+
+#include <list>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "components/gcm_driver/common/gcm_message.h"
+#include "components/gcm_driver/gcm_client.h"
+#include "components/gcm_driver/gcm_profile_service.h"
+
+namespace content {
+class BrowserContext;
+} // namespace content
+
+namespace gcm {
+
+// Acts as a bridge between GCM API and GCM Client layer for testing purposes.
+class FakeGCMProfileService : public GCMProfileService {
+ public:
+ // Helper function to be used with KeyedServiceFactory::SetTestingFactory().
+ static std::unique_ptr<KeyedService> Build(content::BrowserContext* context);
+
+ FakeGCMProfileService();
+
+ FakeGCMProfileService(const FakeGCMProfileService&) = delete;
+ FakeGCMProfileService& operator=(const FakeGCMProfileService&) = delete;
+
+ ~FakeGCMProfileService() override;
+
+ void AddExpectedUnregisterResponse(GCMClient::Result result);
+
+ void DispatchMessage(const std::string& app_id,
+ const IncomingMessage& message);
+
+ const OutgoingMessage& last_sent_message() const {
+ return last_sent_message_;
+ }
+
+ const std::string& last_receiver_id() const { return last_receiver_id_; }
+
+ const std::string& last_registered_app_id() const {
+ return last_registered_app_id_;
+ }
+
+ const std::vector<std::string>& last_registered_sender_ids() const {
+ return last_registered_sender_ids_;
+ }
+
+ // Set whether the service will collect parameters of the calls for further
+ // verification in tests.
+ void set_collect(bool collect) { collect_ = collect; }
+
+ // Crude offline simulation: requests fail and never run their callbacks (in
+ // reality, callbacks run within GetGCMBackoffPolicy().maximum_backoff_ms).
+ void set_offline(bool is_offline) { is_offline_ = is_offline; }
+
+ private:
+ class CustomFakeGCMDriver;
+ friend class CustomFakeGCMDriver;
+
+ bool collect_ = false;
+ bool is_offline_ = false;
+
+ std::string last_registered_app_id_;
+ std::vector<std::string> last_registered_sender_ids_;
+ std::list<GCMClient::Result> unregister_responses_;
+ OutgoingMessage last_sent_message_;
+ std::string last_receiver_id_;
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_FAKE_GCM_PROFILE_SERVICE_H_
diff --git a/chromium/components/gcm_driver/features.cc b/chromium/components/gcm_driver/features.cc
new file mode 100644
index 00000000000..9f6507687b1
--- /dev/null
+++ b/chromium/components/gcm_driver/features.cc
@@ -0,0 +1,41 @@
+// Copyright 2018 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.
+
+#include "components/gcm_driver/features.h"
+#include "base/metrics/field_trial_param_associator.h"
+#include "base/metrics/field_trial_params.h"
+#include "base/strings/string_number_conversions.h"
+
+#include <algorithm>
+#include <map>
+
+namespace gcm {
+
+namespace features {
+
+const base::Feature kInvalidateTokenFeature{"GCMTokenInvalidAfterDays",
+ base::FEATURE_ENABLED_BY_DEFAULT};
+const char kParamNameTokenInvalidationPeriodDays[] =
+ "token_invalidation_period";
+// A token invalidation period of 0 means the feature is disabled, and the
+// GCM token never becomes stale.
+const int kDefaultTokenInvalidationPeriod = 7;
+
+base::TimeDelta GetTokenInvalidationInterval() {
+ if (!base::FeatureList::IsEnabled(kInvalidateTokenFeature))
+ return base::TimeDelta();
+ std::string override_value = base::GetFieldTrialParamValueByFeature(
+ kInvalidateTokenFeature, kParamNameTokenInvalidationPeriodDays);
+
+ if (!override_value.empty()) {
+ int override_value_days;
+ if (base::StringToInt(override_value, &override_value_days))
+ return base::Days(override_value_days);
+ }
+ return base::Days(kDefaultTokenInvalidationPeriod);
+}
+
+} // namespace features
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/features.h b/chromium/components/gcm_driver/features.h
new file mode 100644
index 00000000000..9539efe6e65
--- /dev/null
+++ b/chromium/components/gcm_driver/features.h
@@ -0,0 +1,24 @@
+// Copyright 2018 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 COMPONENTS_GCM_DRIVER_FEATURES_H_
+#define COMPONENTS_GCM_DRIVER_FEATURES_H_
+
+#include "base/feature_list.h"
+
+namespace gcm {
+
+namespace features {
+
+extern const base::Feature kInvalidateTokenFeature;
+extern const char kParamNameTokenInvalidationPeriodDays[];
+
+// The period after which the GCM token becomes stale.
+base::TimeDelta GetTokenInvalidationInterval();
+
+} // namespace features
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_FEATURES_H_
diff --git a/chromium/components/gcm_driver/gcm_account_mapper.cc b/chromium/components/gcm_driver/gcm_account_mapper.cc
new file mode 100644
index 00000000000..5570f8520b3
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_account_mapper.cc
@@ -0,0 +1,386 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/gcm_account_mapper.h"
+
+#include <utility>
+
+#include "base/bind.h"
+#include "base/guid.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/time/clock.h"
+#include "base/time/default_clock.h"
+#include "components/gcm_driver/gcm_driver_desktop.h"
+#include "google_apis/gcm/engine/gcm_store.h"
+
+namespace gcm {
+
+namespace {
+
+const char kGCMAccountMapperSenderId[] = "745476177629";
+const char kGCMAccountMapperSendTo[] = "google.com";
+const int kGCMAddMappingMessageTTL = 30 * 60; // 0.5 hours in seconds.
+const int kGCMRemoveMappingMessageTTL = 24 * 60 * 60; // 1 day in seconds.
+const int kGCMUpdateIntervalHours = 24;
+// Because adding an account mapping dependents on a fresh OAuth2 token, we
+// allow the update to happen earlier than update due time, if it is within
+// the early start time to take advantage of that token.
+const int kGCMUpdateEarlyStartHours = 6;
+const char kRegistrationIdMessgaeKey[] = "id";
+const char kTokenMessageKey[] = "t";
+const char kAccountMessageKey[] = "a";
+const char kRemoveAccountKey[] = "r";
+const char kRemoveAccountValue[] = "1";
+// Use to handle send to Gaia ID scenario:
+const char kGCMSendToGaiaIdAppIdKey[] = "gcmb";
+
+
+std::string GenerateMessageID() {
+ return base::GenerateGUID();
+}
+
+} // namespace
+
+const char kGCMAccountMapperAppId[] = "com.google.android.gms";
+
+GCMAccountMapper::GCMAccountMapper(GCMDriver* gcm_driver)
+ : gcm_driver_(gcm_driver),
+ clock_(base::DefaultClock::GetInstance()),
+ initialized_(false) {}
+
+GCMAccountMapper::~GCMAccountMapper() = default;
+
+void GCMAccountMapper::Initialize(const AccountMappings& account_mappings,
+ const DispatchMessageCallback& callback) {
+ DCHECK(!initialized_);
+ initialized_ = true;
+ accounts_ = account_mappings;
+ dispatch_message_callback_ = callback;
+ GetRegistration();
+}
+
+void GCMAccountMapper::SetAccountTokens(
+ const std::vector<GCMClient::AccountTokenInfo>& account_tokens) {
+ DVLOG(1) << "GCMAccountMapper::SetAccountTokens called with "
+ << account_tokens.size() << " accounts.";
+
+ // If account mapper is not ready to handle tasks yet, save the latest
+ // account tokens and return.
+ if (!IsReady()) {
+ pending_account_tokens_ = account_tokens;
+ // If mapper is initialized, but still does not have registration ID,
+ // maybe the registration gave up. Retrying in case.
+ if (initialized_ && gcm_driver_->IsStarted())
+ GetRegistration();
+ return;
+ }
+
+ // Start from removing the old tokens, from all of the known accounts.
+ for (auto iter = accounts_.begin(); iter != accounts_.end(); ++iter) {
+ iter->access_token.clear();
+ }
+
+ // Update the internal collection of mappings with the new tokens.
+ for (auto token_iter = account_tokens.begin();
+ token_iter != account_tokens.end(); ++token_iter) {
+ AccountMapping* account_mapping =
+ FindMappingByAccountId(token_iter->account_id);
+ if (!account_mapping) {
+ AccountMapping new_mapping;
+ new_mapping.status = AccountMapping::NEW;
+ new_mapping.account_id = token_iter->account_id;
+ new_mapping.access_token = token_iter->access_token;
+ new_mapping.email = token_iter->email;
+ accounts_.push_back(new_mapping);
+ } else {
+ // Since we got a token for an account, drop the remove message and treat
+ // it as mapped.
+ if (account_mapping->status == AccountMapping::REMOVING) {
+ account_mapping->status = AccountMapping::MAPPED;
+ account_mapping->status_change_timestamp = base::Time();
+ account_mapping->last_message_id.clear();
+ }
+
+ account_mapping->email = token_iter->email;
+ account_mapping->access_token = token_iter->access_token;
+ }
+ }
+
+ // Decide what to do with each account (either start mapping, or start
+ // removing).
+ for (auto mappings_iter = accounts_.begin(); mappings_iter != accounts_.end();
+ ++mappings_iter) {
+ if (mappings_iter->access_token.empty()) {
+ // Send a remove message if the account was not previously being removed,
+ // or it doesn't have a pending message, or the pending message is
+ // already expired, but OnSendError event was lost.
+ if (mappings_iter->status != AccountMapping::REMOVING ||
+ mappings_iter->last_message_id.empty() ||
+ IsLastStatusChangeOlderThanTTL(*mappings_iter)) {
+ SendRemoveMappingMessage(*mappings_iter);
+ }
+ } else {
+ // A message is sent for all of the mappings considered NEW, or mappings
+ // that are ADDING, but have expired message (OnSendError event lost), or
+ // for those mapped accounts that can be refreshed.
+ if (mappings_iter->status == AccountMapping::NEW ||
+ (mappings_iter->status == AccountMapping::ADDING &&
+ IsLastStatusChangeOlderThanTTL(*mappings_iter)) ||
+ (mappings_iter->status == AccountMapping::MAPPED &&
+ CanTriggerUpdate(mappings_iter->status_change_timestamp))) {
+ mappings_iter->last_message_id.clear();
+ SendAddMappingMessage(*mappings_iter);
+ }
+ }
+ }
+}
+
+void GCMAccountMapper::ShutdownHandler() {
+ initialized_ = false;
+ accounts_.clear();
+ registration_id_.clear();
+ dispatch_message_callback_.Reset();
+}
+
+void GCMAccountMapper::OnStoreReset() {
+ // TODO(crbug.com/661660): Tell server to remove the mapping. But can't use
+ // upstream GCM send for that since the store got reset.
+ ShutdownHandler();
+}
+
+void GCMAccountMapper::OnMessage(const std::string& app_id,
+ const IncomingMessage& message) {
+ DCHECK_EQ(app_id, kGCMAccountMapperAppId);
+ // TODO(fgorski): Report Send to Gaia ID failures using UMA.
+
+ base::UmaHistogramBoolean("GCM.AccountMappingMessageReceived", true);
+
+ if (dispatch_message_callback_.is_null()) {
+ DVLOG(1) << "dispatch_message_callback_ missing in GCMAccountMapper";
+ return;
+ }
+
+ auto it = message.data.find(kGCMSendToGaiaIdAppIdKey);
+ if (it == message.data.end()) {
+ DVLOG(1) << "Send to Gaia ID failure: Embedded app ID missing.";
+ return;
+ }
+
+ std::string embedded_app_id = it->second;
+ if (embedded_app_id.empty()) {
+ DVLOG(1) << "Send to Gaia ID failure: Embedded app ID is empty.";
+ return;
+ }
+
+ // Ensuring the message does not carry the embedded app ID.
+ IncomingMessage new_message = message;
+ new_message.data.erase(new_message.data.find(kGCMSendToGaiaIdAppIdKey));
+ dispatch_message_callback_.Run(embedded_app_id, new_message);
+}
+
+void GCMAccountMapper::OnMessagesDeleted(const std::string& app_id) {
+ // Account message does not expect messages right now.
+}
+
+void GCMAccountMapper::OnSendError(
+ const std::string& app_id,
+ const GCMClient::SendErrorDetails& send_error_details) {
+ DCHECK_EQ(app_id, kGCMAccountMapperAppId);
+
+ auto account_mapping_it =
+ FindMappingByMessageId(send_error_details.message_id);
+
+ if (account_mapping_it == accounts_.end())
+ return;
+
+ if (send_error_details.result != GCMClient::TTL_EXCEEDED) {
+ DVLOG(1) << "Send error result different than TTL EXCEEDED: "
+ << send_error_details.result << ". "
+ << "Postponing the retry until a new batch of tokens arrives.";
+ return;
+ }
+
+ if (account_mapping_it->status == AccountMapping::REMOVING) {
+ // Another message to remove mapping can be sent immediately, because TTL
+ // for those is one day. No need to back off.
+ SendRemoveMappingMessage(*account_mapping_it);
+ } else {
+ if (account_mapping_it->status == AccountMapping::ADDING) {
+ // There is no mapping established, so we can remove the entry.
+ // Getting a fresh token will trigger a new attempt.
+ gcm_driver_->RemoveAccountMapping(account_mapping_it->account_id);
+ accounts_.erase(account_mapping_it);
+ } else {
+ // Account is already MAPPED, we have to wait for another token.
+ account_mapping_it->last_message_id.clear();
+ gcm_driver_->UpdateAccountMapping(*account_mapping_it);
+ }
+ }
+}
+
+void GCMAccountMapper::OnSendAcknowledged(const std::string& app_id,
+ const std::string& message_id) {
+ DCHECK_EQ(app_id, kGCMAccountMapperAppId);
+ auto account_mapping_it = FindMappingByMessageId(message_id);
+
+ DVLOG(1) << "OnSendAcknowledged with message ID: " << message_id;
+
+ if (account_mapping_it == accounts_.end())
+ return;
+
+ // Here is where we advance a status of a mapping and persist or remove.
+ if (account_mapping_it->status == AccountMapping::REMOVING) {
+ // Message removing the account has been confirmed by the GCM, we can remove
+ // all the information related to the account (from memory and store).
+ gcm_driver_->RemoveAccountMapping(account_mapping_it->account_id);
+ accounts_.erase(account_mapping_it);
+ } else {
+ // Mapping status is ADDING only when it is a first time mapping.
+ DCHECK(account_mapping_it->status == AccountMapping::ADDING ||
+ account_mapping_it->status == AccountMapping::MAPPED);
+
+ // Account is marked as mapped with the current time.
+ account_mapping_it->status = AccountMapping::MAPPED;
+ account_mapping_it->status_change_timestamp = clock_->Now();
+ // There is no pending message for the account.
+ account_mapping_it->last_message_id.clear();
+
+ gcm_driver_->UpdateAccountMapping(*account_mapping_it);
+ }
+}
+
+bool GCMAccountMapper::CanHandle(const std::string& app_id) const {
+ return app_id.compare(kGCMAccountMapperAppId) == 0;
+}
+
+bool GCMAccountMapper::IsReady() {
+ return initialized_ && gcm_driver_->IsStarted() && !registration_id_.empty();
+}
+
+void GCMAccountMapper::SendAddMappingMessage(AccountMapping& account_mapping) {
+ CreateAndSendMessage(account_mapping);
+}
+
+void GCMAccountMapper::SendRemoveMappingMessage(
+ AccountMapping& account_mapping) {
+ // We want to persist an account that is being removed as quickly as possible
+ // as well as clean up the last message information.
+ if (account_mapping.status != AccountMapping::REMOVING) {
+ account_mapping.status = AccountMapping::REMOVING;
+ account_mapping.status_change_timestamp = clock_->Now();
+ }
+
+ account_mapping.last_message_id.clear();
+
+ gcm_driver_->UpdateAccountMapping(account_mapping);
+
+ CreateAndSendMessage(account_mapping);
+}
+
+void GCMAccountMapper::CreateAndSendMessage(
+ const AccountMapping& account_mapping) {
+ OutgoingMessage outgoing_message;
+ outgoing_message.id = GenerateMessageID();
+ outgoing_message.data[kRegistrationIdMessgaeKey] = registration_id_;
+ outgoing_message.data[kAccountMessageKey] = account_mapping.email;
+
+ if (account_mapping.status == AccountMapping::REMOVING) {
+ outgoing_message.time_to_live = kGCMRemoveMappingMessageTTL;
+ outgoing_message.data[kRemoveAccountKey] = kRemoveAccountValue;
+ } else {
+ outgoing_message.data[kTokenMessageKey] = account_mapping.access_token;
+ outgoing_message.time_to_live = kGCMAddMappingMessageTTL;
+ }
+
+ gcm_driver_->Send(kGCMAccountMapperAppId, kGCMAccountMapperSendTo,
+ outgoing_message,
+ base::BindOnce(&GCMAccountMapper::OnSendFinished,
+ weak_ptr_factory_.GetWeakPtr(),
+ account_mapping.account_id));
+}
+
+void GCMAccountMapper::OnSendFinished(const CoreAccountId& account_id,
+ const std::string& message_id,
+ GCMClient::Result result) {
+ // TODO(fgorski): Add another attempt, in case the QUEUE is not full.
+ if (result != GCMClient::SUCCESS)
+ return;
+
+ AccountMapping* account_mapping = FindMappingByAccountId(account_id);
+ DCHECK(account_mapping);
+
+ // If we are dealing with account with status NEW, it is the first time
+ // mapping, and we should mark it as ADDING.
+ if (account_mapping->status == AccountMapping::NEW) {
+ account_mapping->status = AccountMapping::ADDING;
+ account_mapping->status_change_timestamp = clock_->Now();
+ }
+
+ account_mapping->last_message_id = message_id;
+
+ gcm_driver_->UpdateAccountMapping(*account_mapping);
+}
+
+void GCMAccountMapper::GetRegistration() {
+ DCHECK(registration_id_.empty());
+ std::vector<std::string> sender_ids;
+ sender_ids.push_back(kGCMAccountMapperSenderId);
+ gcm_driver_->Register(kGCMAccountMapperAppId, sender_ids,
+ base::BindOnce(&GCMAccountMapper::OnRegisterFinished,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+void GCMAccountMapper::OnRegisterFinished(const std::string& registration_id,
+ GCMClient::Result result) {
+ if (result == GCMClient::SUCCESS)
+ registration_id_ = registration_id;
+
+ if (IsReady()) {
+ if (!pending_account_tokens_.empty()) {
+ SetAccountTokens(pending_account_tokens_);
+ pending_account_tokens_.clear();
+ }
+ }
+}
+
+bool GCMAccountMapper::CanTriggerUpdate(
+ const base::Time& last_update_time) const {
+ return last_update_time +
+ base::Hours(kGCMUpdateIntervalHours - kGCMUpdateEarlyStartHours) <
+ clock_->Now();
+}
+
+bool GCMAccountMapper::IsLastStatusChangeOlderThanTTL(
+ const AccountMapping& account_mapping) const {
+ int ttl_seconds = account_mapping.status == AccountMapping::REMOVING ?
+ kGCMRemoveMappingMessageTTL : kGCMAddMappingMessageTTL;
+ return account_mapping.status_change_timestamp + base::Seconds(ttl_seconds) <
+ clock_->Now();
+}
+
+AccountMapping* GCMAccountMapper::FindMappingByAccountId(
+ const CoreAccountId& account_id) {
+ for (auto iter = accounts_.begin(); iter != accounts_.end(); ++iter) {
+ if (iter->account_id == account_id)
+ return &*iter;
+ }
+
+ return nullptr;
+}
+
+GCMAccountMapper::AccountMappings::iterator
+GCMAccountMapper::FindMappingByMessageId(const std::string& message_id) {
+ for (auto iter = accounts_.begin(); iter != accounts_.end(); ++iter) {
+ if (iter->last_message_id == message_id)
+ return iter;
+ }
+
+ return accounts_.end();
+}
+
+void GCMAccountMapper::SetClockForTesting(base::Clock* clock) {
+ clock_ = clock;
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_account_mapper.h b/chromium/components/gcm_driver/gcm_account_mapper.h
new file mode 100644
index 00000000000..9b4ab1f56f6
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_account_mapper.h
@@ -0,0 +1,136 @@
+// Copyright 2014 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 COMPONENTS_GCM_DRIVER_GCM_ACCOUNT_MAPPER_H_
+#define COMPONENTS_GCM_DRIVER_GCM_ACCOUNT_MAPPER_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/callback.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "components/gcm_driver/gcm_app_handler.h"
+#include "components/gcm_driver/gcm_client.h"
+#include "google_apis/gcm/engine/account_mapping.h"
+
+namespace base {
+class Clock;
+}
+
+namespace gcm {
+
+class GCMDriver;
+extern const char kGCMAccountMapperAppId[];
+
+// Class for mapping signed-in GAIA accounts to the GCM Device ID.
+class GCMAccountMapper : public GCMAppHandler {
+ public:
+ // List of account mappings.
+ using AccountMappings = std::vector<AccountMapping>;
+ using DispatchMessageCallback =
+ base::RepeatingCallback<void(const std::string& app_id,
+ const IncomingMessage& message)>;
+
+ explicit GCMAccountMapper(GCMDriver* gcm_driver);
+
+ GCMAccountMapper(const GCMAccountMapper&) = delete;
+ GCMAccountMapper& operator=(const GCMAccountMapper&) = delete;
+
+ ~GCMAccountMapper() override;
+
+ void Initialize(const AccountMappings& account_mappings,
+ const DispatchMessageCallback& callback);
+
+ // Called by AccountTracker, when a new list of account tokens is available.
+ // This will cause a refresh of account mappings and sending updates to GCM.
+ void SetAccountTokens(
+ const std::vector<GCMClient::AccountTokenInfo>& account_tokens);
+
+ // Implementation of GCMAppHandler:
+ void ShutdownHandler() override;
+ void OnStoreReset() override;
+ void OnMessage(const std::string& app_id,
+ const IncomingMessage& message) override;
+ void OnMessagesDeleted(const std::string& app_id) override;
+ void OnSendError(
+ const std::string& app_id,
+ const GCMClient::SendErrorDetails& send_error_details) override;
+ void OnSendAcknowledged(const std::string& app_id,
+ const std::string& message_id) override;
+ bool CanHandle(const std::string& app_id) const override;
+
+ private:
+ friend class GCMAccountMapperTest;
+
+ typedef std::map<std::string, OutgoingMessage> OutgoingMessages;
+
+ // Checks whether account mapper is ready to process new account tokens.
+ bool IsReady();
+
+ // Informs GCM of an added or refreshed account mapping.
+ void SendAddMappingMessage(AccountMapping& account_mapping);
+
+ // Informs GCM of a removed account mapping.
+ void SendRemoveMappingMessage(AccountMapping& account_mapping);
+
+ void CreateAndSendMessage(const AccountMapping& account_mapping);
+
+ // Callback for sending a message.
+ void OnSendFinished(const CoreAccountId& account_id,
+ const std::string& message_id,
+ GCMClient::Result result);
+
+ // Gets a registration for account mapper from GCM.
+ void GetRegistration();
+
+ // Callback for registering with GCM.
+ void OnRegisterFinished(const std::string& registration_id,
+ GCMClient::Result result);
+
+ // Checks whether the update can be triggered now. If the current time is
+ // within reasonable time (6 hours) of when the update is due, we want to
+ // trigger the update immediately to take advantage of a fresh OAuth2 token.
+ bool CanTriggerUpdate(const base::Time& last_update_time) const;
+
+ // Checks whether last status change is older than a TTL of a message.
+ bool IsLastStatusChangeOlderThanTTL(
+ const AccountMapping& account_mapping) const;
+
+ // Finds an account mapping in |accounts_| by |account_id|.
+ AccountMapping* FindMappingByAccountId(const CoreAccountId& account_id);
+ // Finds an account mapping in |accounts_| by |message_id|.
+ // Returns iterator that can be used to delete the account.
+ AccountMappings::iterator FindMappingByMessageId(
+ const std::string& message_id);
+
+ // Sets the clock for testing.
+ void SetClockForTesting(base::Clock* clock);
+
+ // GCMDriver owns GCMAccountMapper.
+ raw_ptr<GCMDriver> gcm_driver_;
+
+ // Callback to GCMDriver to dispatch messages sent to Gaia ID.
+ DispatchMessageCallback dispatch_message_callback_;
+
+ // Clock for timestamping status changes.
+ raw_ptr<base::Clock> clock_;
+
+ // Currnetly tracked account mappings.
+ AccountMappings accounts_;
+
+ std::vector<GCMClient::AccountTokenInfo> pending_account_tokens_;
+
+ // GCM Registration ID of the account mapper.
+ std::string registration_id_;
+
+ bool initialized_;
+
+ base::WeakPtrFactory<GCMAccountMapper> weak_ptr_factory_{this};
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_ACCOUNT_MAPPER_H_
diff --git a/chromium/components/gcm_driver/gcm_account_mapper_unittest.cc b/chromium/components/gcm_driver/gcm_account_mapper_unittest.cc
new file mode 100644
index 00000000000..286d7e41915
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_account_mapper_unittest.cc
@@ -0,0 +1,955 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/gcm_account_mapper.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/test/simple_test_clock.h"
+#include "base/time/time.h"
+#include "components/gcm_driver/fake_gcm_driver.h"
+#include "google_apis/gcm/engine/account_mapping.h"
+#include "google_apis/gcm/engine/gcm_store.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+
+const char kGCMAccountMapperSenderId[] = "745476177629";
+const char kGCMAccountMapperSendTo[] = "google.com";
+const char kRegistrationId[] = "reg_id";
+const char kEmbeddedAppIdKey[] = "gcmb";
+const char kTestAppId[] = "test_app_id";
+const char kTestDataKey[] = "data_key";
+const char kTestDataValue[] = "data_value";
+const char kTestCollapseKey[] = "test_collapse_key";
+const char kTestSenderId[] = "test_sender_id";
+
+AccountMapping MakeAccountMapping(const CoreAccountId& account_id,
+ AccountMapping::MappingStatus status,
+ const base::Time& status_change_timestamp,
+ const std::string& last_message_id) {
+ AccountMapping account_mapping;
+ account_mapping.account_id = account_id;
+ account_mapping.email = account_id.ToString() + "@gmail.com";
+ // account_mapping.access_token intentionally left empty.
+ account_mapping.status = status;
+ account_mapping.status_change_timestamp = status_change_timestamp;
+ account_mapping.last_message_id = last_message_id;
+ return account_mapping;
+}
+
+GCMClient::AccountTokenInfo MakeAccountTokenInfo(
+ const CoreAccountId& account_id) {
+ GCMClient::AccountTokenInfo account_token;
+ account_token.account_id = account_id;
+ account_token.email = account_id.ToString() + "@gmail.com";
+ account_token.access_token = account_id.ToString() + "_token";
+ return account_token;
+}
+
+void VerifyMappings(const GCMAccountMapper::AccountMappings& expected_mappings,
+ const GCMAccountMapper::AccountMappings& actual_mappings,
+ const std::string& verification_info) {
+ EXPECT_EQ(expected_mappings.size(), actual_mappings.size())
+ << "Verification Info: " << verification_info;
+ auto expected_iter = expected_mappings.begin();
+ auto actual_iter = actual_mappings.begin();
+ for (; expected_iter != expected_mappings.end() &&
+ actual_iter != actual_mappings.end();
+ ++expected_iter, ++actual_iter) {
+ EXPECT_EQ(expected_iter->email, actual_iter->email)
+ << "Verification Info: " << verification_info
+ << "; Account ID of expected: " << expected_iter->account_id;
+ EXPECT_EQ(expected_iter->account_id, actual_iter->account_id)
+ << "Verification Info: " << verification_info;
+ EXPECT_EQ(expected_iter->status, actual_iter->status)
+ << "Verification Info: " << verification_info
+ << "; Account ID of expected: " << expected_iter->account_id;
+ EXPECT_EQ(expected_iter->status_change_timestamp,
+ actual_iter->status_change_timestamp)
+ << "Verification Info: " << verification_info
+ << "; Account ID of expected: " << expected_iter->account_id;
+ }
+}
+
+class CustomFakeGCMDriver : public FakeGCMDriver {
+ public:
+ enum LastMessageAction {
+ NONE,
+ SEND_STARTED,
+ SEND_FINISHED,
+ SEND_ACKNOWLEDGED
+ };
+
+ CustomFakeGCMDriver();
+ ~CustomFakeGCMDriver() override;
+
+ // GCMDriver implementation:
+ void UpdateAccountMapping(const AccountMapping& account_mapping) override;
+ void RemoveAccountMapping(const CoreAccountId& account_id) override;
+ void RegisterImpl(const std::string& app_id,
+ const std::vector<std::string>& sender_ids) override;
+
+ void CompleteRegister(const std::string& registration_id,
+ GCMClient::Result result);
+ void CompleteSend(const std::string& message_id, GCMClient::Result result);
+ void AcknowledgeSend(const std::string& message_id);
+ void MessageSendError(const std::string& message_id);
+
+ void CompleteSendAllMessages();
+ void AcknowledgeSendAllMessages();
+ void SetLastMessageAction(const std::string& message_id,
+ LastMessageAction action);
+ void Clear();
+
+ const AccountMapping& last_account_mapping() const {
+ return account_mapping_;
+ }
+ const std::string& last_message_id() const { return last_message_id_; }
+ const CoreAccountId& last_removed_account_id() const {
+ return last_removed_account_id_;
+ }
+ LastMessageAction last_action() const { return last_action_; }
+ bool registration_id_requested() const { return registration_id_requested_; }
+
+ protected:
+ void SendImpl(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) override;
+
+ private:
+ AccountMapping account_mapping_;
+ std::string last_message_id_;
+ CoreAccountId last_removed_account_id_;
+ LastMessageAction last_action_;
+ std::map<std::string, LastMessageAction> all_messages_;
+ bool registration_id_requested_;
+};
+
+CustomFakeGCMDriver::CustomFakeGCMDriver()
+ : last_action_(NONE), registration_id_requested_(false) {
+}
+
+CustomFakeGCMDriver::~CustomFakeGCMDriver() {
+}
+
+void CustomFakeGCMDriver::UpdateAccountMapping(
+ const AccountMapping& account_mapping) {
+ account_mapping_.email = account_mapping.email;
+ account_mapping_.account_id = account_mapping.account_id;
+ account_mapping_.access_token = account_mapping.access_token;
+ account_mapping_.status = account_mapping.status;
+ account_mapping_.status_change_timestamp =
+ account_mapping.status_change_timestamp;
+ account_mapping_.last_message_id = account_mapping.last_message_id;
+}
+
+void CustomFakeGCMDriver::RemoveAccountMapping(
+ const CoreAccountId& account_id) {
+ last_removed_account_id_ = account_id;
+}
+
+void CustomFakeGCMDriver::RegisterImpl(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids) {
+ DCHECK_EQ(kGCMAccountMapperAppId, app_id);
+ DCHECK_EQ(1u, sender_ids.size());
+ DCHECK_EQ(kGCMAccountMapperSenderId, sender_ids[0]);
+ registration_id_requested_ = true;
+}
+
+void CustomFakeGCMDriver::CompleteRegister(const std::string& registration_id,
+ GCMClient::Result result) {
+ RegisterFinished(kGCMAccountMapperAppId, registration_id, result);
+}
+
+void CustomFakeGCMDriver::CompleteSend(const std::string& message_id,
+ GCMClient::Result result) {
+ SendFinished(kGCMAccountMapperAppId, message_id, result);
+ SetLastMessageAction(message_id, SEND_FINISHED);
+}
+
+void CustomFakeGCMDriver::AcknowledgeSend(const std::string& message_id) {
+ GCMAppHandler* handler = GetAppHandler(kGCMAccountMapperAppId);
+ if (handler)
+ handler->OnSendAcknowledged(kGCMAccountMapperAppId, message_id);
+ SetLastMessageAction(message_id, SEND_ACKNOWLEDGED);
+}
+
+void CustomFakeGCMDriver::MessageSendError(const std::string& message_id) {
+ GCMAppHandler* handler = GetAppHandler(kGCMAccountMapperAppId);
+ if (!handler)
+ return;
+
+ GCMClient::SendErrorDetails send_error;
+ send_error.message_id = message_id;
+ send_error.result = GCMClient::TTL_EXCEEDED;
+
+ handler->OnSendError(kGCMAccountMapperAppId, send_error);
+}
+
+void CustomFakeGCMDriver::SendImpl(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) {
+ DCHECK_EQ(kGCMAccountMapperAppId, app_id);
+ DCHECK_EQ(kGCMAccountMapperSendTo, receiver_id);
+
+ SetLastMessageAction(message.id, SEND_STARTED);
+}
+
+void CustomFakeGCMDriver::CompleteSendAllMessages() {
+ for (std::map<std::string, LastMessageAction>::const_iterator iter =
+ all_messages_.begin();
+ iter != all_messages_.end();
+ ++iter) {
+ if (iter->second == SEND_STARTED)
+ CompleteSend(iter->first, GCMClient::SUCCESS);
+ }
+}
+
+void CustomFakeGCMDriver::AcknowledgeSendAllMessages() {
+ for (std::map<std::string, LastMessageAction>::const_iterator iter =
+ all_messages_.begin();
+ iter != all_messages_.end();
+ ++iter) {
+ if (iter->second == SEND_FINISHED)
+ AcknowledgeSend(iter->first);
+ }
+}
+
+void CustomFakeGCMDriver::Clear() {
+ account_mapping_ = AccountMapping();
+ last_message_id_.clear();
+ last_removed_account_id_ = CoreAccountId();
+ last_action_ = NONE;
+ registration_id_requested_ = false;
+}
+
+void CustomFakeGCMDriver::SetLastMessageAction(const std::string& message_id,
+ LastMessageAction action) {
+ last_action_ = action;
+ last_message_id_ = message_id;
+ all_messages_[message_id] = action;
+}
+
+} // namespace
+
+class GCMAccountMapperTest : public testing::Test {
+ public:
+ const CoreAccountId kAccountId;
+ const CoreAccountId kAccountId1;
+ const CoreAccountId kAccountId2;
+ const CoreAccountId kAccountId3;
+ const CoreAccountId kAccountId4;
+
+ GCMAccountMapperTest();
+ ~GCMAccountMapperTest() override;
+
+ void Restart();
+
+ void Initialize(const GCMAccountMapper::AccountMappings mappings);
+ const GCMAccountMapper::AccountMappings& GetAccounts() const {
+ return account_mapper_->accounts_;
+ }
+ void MessageReceived(const std::string& app_id,
+ const IncomingMessage& message);
+
+ GCMAccountMapper* mapper() { return account_mapper_.get(); }
+
+ CustomFakeGCMDriver& gcm_driver() { return gcm_driver_; }
+
+ base::SimpleTestClock* clock() { return &clock_; }
+ const std::string& last_received_app_id() const {
+ return last_received_app_id_;
+ }
+ const IncomingMessage& last_received_message() const {
+ return last_received_message_;
+ }
+
+ private:
+ CustomFakeGCMDriver gcm_driver_;
+ std::unique_ptr<GCMAccountMapper> account_mapper_;
+ base::SimpleTestClock clock_;
+ std::string last_received_app_id_;
+ IncomingMessage last_received_message_;
+};
+
+GCMAccountMapperTest::GCMAccountMapperTest()
+ : kAccountId("acc_id"),
+ kAccountId1("acc_id1"),
+ kAccountId2("acc_id2"),
+ kAccountId3("acc_id3"),
+ kAccountId4("acc_id4") {
+ Restart();
+}
+
+GCMAccountMapperTest::~GCMAccountMapperTest() {
+}
+
+void GCMAccountMapperTest::Restart() {
+ if (account_mapper_)
+ account_mapper_->ShutdownHandler();
+ gcm_driver_.RemoveAppHandler(kGCMAccountMapperAppId);
+ account_mapper_ = std::make_unique<GCMAccountMapper>(&gcm_driver_);
+ account_mapper_->SetClockForTesting(&clock_);
+}
+
+void GCMAccountMapperTest::Initialize(
+ const GCMAccountMapper::AccountMappings mappings) {
+ mapper()->Initialize(
+ mappings, base::BindRepeating(&GCMAccountMapperTest::MessageReceived,
+ base::Unretained(this)));
+}
+
+void GCMAccountMapperTest::MessageReceived(const std::string& app_id,
+ const IncomingMessage& message) {
+ last_received_app_id_ = app_id;
+ last_received_message_ = message;
+}
+
+// Tests the initialization of account mappings (from the store) when empty.
+// It also checks that initialization triggers registration ID request.
+TEST_F(GCMAccountMapperTest, InitializeAccountMappingsEmpty) {
+ EXPECT_FALSE(gcm_driver().registration_id_requested());
+ Initialize(GCMAccountMapper::AccountMappings());
+ EXPECT_TRUE(GetAccounts().empty());
+ EXPECT_TRUE(gcm_driver().registration_id_requested());
+}
+
+// Tests that registration is retried, when new tokens are delivered and in no
+// other circumstances.
+TEST_F(GCMAccountMapperTest, RegistrationRetryUponFailure) {
+ Initialize(GCMAccountMapper::AccountMappings());
+ EXPECT_TRUE(gcm_driver().registration_id_requested());
+ gcm_driver().Clear();
+
+ gcm_driver().CompleteRegister(kRegistrationId, GCMClient::UNKNOWN_ERROR);
+ EXPECT_FALSE(gcm_driver().registration_id_requested());
+ gcm_driver().Clear();
+
+ std::vector<GCMClient::AccountTokenInfo> account_tokens;
+ account_tokens.push_back(MakeAccountTokenInfo(kAccountId2));
+ mapper()->SetAccountTokens(account_tokens);
+ EXPECT_TRUE(gcm_driver().registration_id_requested());
+ gcm_driver().Clear();
+
+ gcm_driver().CompleteRegister(kRegistrationId,
+ GCMClient::ASYNC_OPERATION_PENDING);
+ EXPECT_FALSE(gcm_driver().registration_id_requested());
+}
+
+// Tests the initialization of account mappings (from the store).
+TEST_F(GCMAccountMapperTest, InitializeAccountMappings) {
+ GCMAccountMapper::AccountMappings account_mappings;
+ AccountMapping account_mapping1 = MakeAccountMapping(
+ kAccountId1, AccountMapping::MAPPED, base::Time::Now(), std::string());
+ AccountMapping account_mapping2 = MakeAccountMapping(
+ kAccountId2, AccountMapping::ADDING, base::Time::Now(), "add_message_1");
+ account_mappings.push_back(account_mapping1);
+ account_mappings.push_back(account_mapping2);
+
+ Initialize(account_mappings);
+
+ GCMAccountMapper::AccountMappings mappings = GetAccounts();
+ EXPECT_EQ(2UL, mappings.size());
+ GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin();
+
+ EXPECT_EQ(account_mapping1.account_id, iter->account_id);
+ EXPECT_EQ(account_mapping1.email, iter->email);
+ EXPECT_TRUE(account_mapping1.access_token.empty());
+ EXPECT_EQ(account_mapping1.status, iter->status);
+ EXPECT_EQ(account_mapping1.status_change_timestamp,
+ iter->status_change_timestamp);
+ EXPECT_TRUE(account_mapping1.last_message_id.empty());
+
+ ++iter;
+ EXPECT_EQ(account_mapping2.account_id, iter->account_id);
+ EXPECT_EQ(account_mapping2.email, iter->email);
+ EXPECT_TRUE(account_mapping2.access_token.empty());
+ EXPECT_EQ(account_mapping2.status, iter->status);
+ EXPECT_EQ(account_mapping2.status_change_timestamp,
+ iter->status_change_timestamp);
+ EXPECT_EQ(account_mapping2.last_message_id, iter->last_message_id);
+}
+
+// Tests that account tokens are not processed until registration ID is
+// available.
+TEST_F(GCMAccountMapperTest, SetAccountTokensOnlyWorksWithRegisterationId) {
+ // Start with empty list.
+ Initialize(GCMAccountMapper::AccountMappings());
+
+ std::vector<GCMClient::AccountTokenInfo> account_tokens;
+ account_tokens.push_back(MakeAccountTokenInfo(kAccountId));
+ mapper()->SetAccountTokens(account_tokens);
+
+ EXPECT_TRUE(GetAccounts().empty());
+
+ account_tokens.clear();
+ account_tokens.push_back(MakeAccountTokenInfo(kAccountId1));
+ account_tokens.push_back(MakeAccountTokenInfo(kAccountId2));
+ mapper()->SetAccountTokens(account_tokens);
+
+ EXPECT_TRUE(GetAccounts().empty());
+
+ gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS);
+
+ GCMAccountMapper::AccountMappings mappings = GetAccounts();
+ EXPECT_EQ(2UL, mappings.size());
+ EXPECT_EQ(kAccountId1, mappings[0].account_id);
+ EXPECT_EQ(kAccountId2, mappings[1].account_id);
+}
+
+// Tests the part where a new account is added with a token, to the point when
+// GCM message is sent.
+TEST_F(GCMAccountMapperTest, AddMappingToMessageSent) {
+ Initialize(GCMAccountMapper::AccountMappings());
+ gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS);
+
+ std::vector<GCMClient::AccountTokenInfo> account_tokens;
+ GCMClient::AccountTokenInfo account_token = MakeAccountTokenInfo(kAccountId);
+ account_tokens.push_back(account_token);
+ mapper()->SetAccountTokens(account_tokens);
+
+ GCMAccountMapper::AccountMappings mappings = GetAccounts();
+ EXPECT_EQ(1UL, mappings.size());
+ GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin();
+ EXPECT_EQ(kAccountId, iter->account_id);
+ EXPECT_EQ("acc_id@gmail.com", iter->email);
+ EXPECT_EQ("acc_id_token", iter->access_token);
+ EXPECT_EQ(AccountMapping::NEW, iter->status);
+ EXPECT_EQ(base::Time(), iter->status_change_timestamp);
+
+ EXPECT_TRUE(!gcm_driver().last_message_id().empty());
+}
+
+// Tests the part where GCM message is successfully queued.
+TEST_F(GCMAccountMapperTest, AddMappingMessageQueued) {
+ Initialize(GCMAccountMapper::AccountMappings());
+ gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS);
+
+ std::vector<GCMClient::AccountTokenInfo> account_tokens;
+ GCMClient::AccountTokenInfo account_token = MakeAccountTokenInfo(kAccountId);
+ account_tokens.push_back(account_token);
+ mapper()->SetAccountTokens(account_tokens);
+
+ clock()->SetNow(base::Time::Now());
+ gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS);
+
+ EXPECT_EQ(account_token.email, gcm_driver().last_account_mapping().email);
+ EXPECT_EQ(account_token.account_id,
+ gcm_driver().last_account_mapping().account_id);
+ EXPECT_EQ(account_token.access_token,
+ gcm_driver().last_account_mapping().access_token);
+ EXPECT_EQ(AccountMapping::ADDING, gcm_driver().last_account_mapping().status);
+ EXPECT_EQ(clock()->Now(),
+ gcm_driver().last_account_mapping().status_change_timestamp);
+ EXPECT_EQ(gcm_driver().last_message_id(),
+ gcm_driver().last_account_mapping().last_message_id);
+
+ GCMAccountMapper::AccountMappings mappings = GetAccounts();
+ GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin();
+ EXPECT_EQ(account_token.email, iter->email);
+ EXPECT_EQ(account_token.account_id, iter->account_id);
+ EXPECT_EQ(account_token.access_token, iter->access_token);
+ EXPECT_EQ(AccountMapping::ADDING, iter->status);
+ EXPECT_EQ(clock()->Now(), iter->status_change_timestamp);
+ EXPECT_EQ(gcm_driver().last_message_id(), iter->last_message_id);
+}
+
+// Tests status change from ADDING to MAPPED (Message is acknowledged).
+TEST_F(GCMAccountMapperTest, AddMappingMessageAcknowledged) {
+ Initialize(GCMAccountMapper::AccountMappings());
+ gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper());
+ gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS);
+
+ std::vector<GCMClient::AccountTokenInfo> account_tokens;
+ GCMClient::AccountTokenInfo account_token = MakeAccountTokenInfo(kAccountId);
+ account_tokens.push_back(account_token);
+ mapper()->SetAccountTokens(account_tokens);
+
+ clock()->SetNow(base::Time::Now());
+ gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS);
+ clock()->SetNow(base::Time::Now());
+ gcm_driver().AcknowledgeSend(gcm_driver().last_message_id());
+
+ EXPECT_EQ(account_token.email, gcm_driver().last_account_mapping().email);
+ EXPECT_EQ(account_token.account_id,
+ gcm_driver().last_account_mapping().account_id);
+ EXPECT_EQ(account_token.access_token,
+ gcm_driver().last_account_mapping().access_token);
+ EXPECT_EQ(AccountMapping::MAPPED, gcm_driver().last_account_mapping().status);
+ EXPECT_EQ(clock()->Now(),
+ gcm_driver().last_account_mapping().status_change_timestamp);
+ EXPECT_TRUE(gcm_driver().last_account_mapping().last_message_id.empty());
+
+ GCMAccountMapper::AccountMappings mappings = GetAccounts();
+ GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin();
+ EXPECT_EQ(account_token.email, iter->email);
+ EXPECT_EQ(account_token.account_id, iter->account_id);
+ EXPECT_EQ(account_token.access_token, iter->access_token);
+ EXPECT_EQ(AccountMapping::MAPPED, iter->status);
+ EXPECT_EQ(clock()->Now(), iter->status_change_timestamp);
+ EXPECT_TRUE(iter->last_message_id.empty());
+}
+
+// Tests status change form ADDING to MAPPED (When message was acknowledged,
+// after Chrome was restarted).
+TEST_F(GCMAccountMapperTest, AddMappingMessageAckedAfterRestart) {
+ Initialize(GCMAccountMapper::AccountMappings());
+ gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper());
+ gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS);
+
+ std::vector<GCMClient::AccountTokenInfo> account_tokens;
+ GCMClient::AccountTokenInfo account_token = MakeAccountTokenInfo(kAccountId);
+ account_tokens.push_back(account_token);
+ mapper()->SetAccountTokens(account_tokens);
+
+ clock()->SetNow(base::Time::Now());
+ gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS);
+
+ Restart();
+ GCMAccountMapper::AccountMappings stored_mappings;
+ stored_mappings.push_back(gcm_driver().last_account_mapping());
+ Initialize(stored_mappings);
+ gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper());
+
+ clock()->SetNow(base::Time::Now());
+ gcm_driver().AcknowledgeSend(gcm_driver().last_message_id());
+
+ EXPECT_EQ(account_token.email, gcm_driver().last_account_mapping().email);
+ EXPECT_EQ(account_token.account_id,
+ gcm_driver().last_account_mapping().account_id);
+ EXPECT_EQ(account_token.access_token,
+ gcm_driver().last_account_mapping().access_token);
+ EXPECT_EQ(AccountMapping::MAPPED, gcm_driver().last_account_mapping().status);
+ EXPECT_EQ(clock()->Now(),
+ gcm_driver().last_account_mapping().status_change_timestamp);
+ EXPECT_TRUE(gcm_driver().last_account_mapping().last_message_id.empty());
+
+ GCMAccountMapper::AccountMappings mappings = GetAccounts();
+ GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin();
+ EXPECT_EQ(account_token.email, iter->email);
+ EXPECT_EQ(account_token.account_id, iter->account_id);
+ EXPECT_EQ(account_token.access_token, iter->access_token);
+ EXPECT_EQ(AccountMapping::MAPPED, iter->status);
+ EXPECT_EQ(clock()->Now(), iter->status_change_timestamp);
+ EXPECT_TRUE(iter->last_message_id.empty());
+}
+
+// Tests a case when ADD message times out for a new account.
+TEST_F(GCMAccountMapperTest, AddMappingMessageSendErrorForNewAccount) {
+ Initialize(GCMAccountMapper::AccountMappings());
+ gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper());
+ gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS);
+
+ std::vector<GCMClient::AccountTokenInfo> account_tokens;
+ GCMClient::AccountTokenInfo account_token = MakeAccountTokenInfo(kAccountId);
+ account_tokens.push_back(account_token);
+ mapper()->SetAccountTokens(account_tokens);
+
+ clock()->SetNow(base::Time::Now());
+ gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS);
+
+ clock()->SetNow(base::Time::Now());
+ std::string old_message_id = gcm_driver().last_message_id();
+ gcm_driver().MessageSendError(old_message_id);
+
+ // No new message is sent because of the send error, as the token is stale.
+ // Because the account was new, the entry should be deleted.
+ EXPECT_EQ(old_message_id, gcm_driver().last_message_id());
+ EXPECT_EQ(account_token.account_id, gcm_driver().last_removed_account_id());
+ EXPECT_TRUE(GetAccounts().empty());
+}
+
+/// Tests a case when ADD message times out for a MAPPED account.
+TEST_F(GCMAccountMapperTest, AddMappingMessageSendErrorForMappedAccount) {
+ // Start with one account that is mapped.
+ base::Time status_change_timestamp = base::Time::Now();
+ AccountMapping mapping =
+ MakeAccountMapping(kAccountId, AccountMapping::MAPPED,
+ status_change_timestamp, "add_message_id");
+
+ GCMAccountMapper::AccountMappings stored_mappings;
+ stored_mappings.push_back(mapping);
+ Initialize(stored_mappings);
+ gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper());
+ gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS);
+
+ clock()->SetNow(base::Time::Now());
+ gcm_driver().MessageSendError("add_message_id");
+
+ // No new message is sent because of the send error, as the token is stale.
+ // Because the account was new, the entry should be deleted.
+ EXPECT_TRUE(gcm_driver().last_message_id().empty());
+
+ GCMAccountMapper::AccountMappings mappings = GetAccounts();
+ GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin();
+ EXPECT_EQ(mapping.email, iter->email);
+ EXPECT_EQ(mapping.account_id, iter->account_id);
+ EXPECT_EQ(mapping.access_token, iter->access_token);
+ EXPECT_EQ(AccountMapping::MAPPED, iter->status);
+ EXPECT_EQ(status_change_timestamp, iter->status_change_timestamp);
+ EXPECT_TRUE(iter->last_message_id.empty());
+}
+
+// Tests that a missing token for an account will trigger removing of that
+// account. This test goes only until the message is passed to GCM.
+TEST_F(GCMAccountMapperTest, RemoveMappingToMessageSent) {
+ // Start with one account that is mapped.
+ AccountMapping mapping = MakeAccountMapping(
+ kAccountId, AccountMapping::MAPPED, base::Time::Now(), std::string());
+
+ GCMAccountMapper::AccountMappings stored_mappings;
+ stored_mappings.push_back(mapping);
+ Initialize(stored_mappings);
+ gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS);
+ clock()->SetNow(base::Time::Now());
+
+ mapper()->SetAccountTokens(std::vector<GCMClient::AccountTokenInfo>());
+
+ EXPECT_EQ(mapping.account_id, gcm_driver().last_account_mapping().account_id);
+ EXPECT_EQ(mapping.email, gcm_driver().last_account_mapping().email);
+ EXPECT_EQ(AccountMapping::REMOVING,
+ gcm_driver().last_account_mapping().status);
+ EXPECT_EQ(clock()->Now(),
+ gcm_driver().last_account_mapping().status_change_timestamp);
+ EXPECT_TRUE(gcm_driver().last_account_mapping().last_message_id.empty());
+
+ GCMAccountMapper::AccountMappings mappings = GetAccounts();
+ GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin();
+ EXPECT_EQ(mapping.email, iter->email);
+ EXPECT_EQ(mapping.account_id, iter->account_id);
+ EXPECT_EQ(mapping.access_token, iter->access_token);
+ EXPECT_EQ(AccountMapping::REMOVING, iter->status);
+ EXPECT_EQ(clock()->Now(), iter->status_change_timestamp);
+ EXPECT_TRUE(iter->last_message_id.empty());
+}
+
+// Tests that a missing token for an account will trigger removing of that
+// account. This test goes until the message is queued by GCM.
+TEST_F(GCMAccountMapperTest, RemoveMappingMessageQueued) {
+ // Start with one account that is mapped.
+ AccountMapping mapping = MakeAccountMapping(
+ kAccountId, AccountMapping::MAPPED, base::Time::Now(), std::string());
+
+ GCMAccountMapper::AccountMappings stored_mappings;
+ stored_mappings.push_back(mapping);
+ Initialize(stored_mappings);
+ gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS);
+ clock()->SetNow(base::Time::Now());
+ base::Time status_change_timestamp = clock()->Now();
+
+ mapper()->SetAccountTokens(std::vector<GCMClient::AccountTokenInfo>());
+ clock()->SetNow(base::Time::Now());
+ gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS);
+
+ EXPECT_EQ(mapping.account_id, gcm_driver().last_account_mapping().account_id);
+ EXPECT_EQ(mapping.email, gcm_driver().last_account_mapping().email);
+ EXPECT_EQ(AccountMapping::REMOVING,
+ gcm_driver().last_account_mapping().status);
+ EXPECT_EQ(status_change_timestamp,
+ gcm_driver().last_account_mapping().status_change_timestamp);
+ EXPECT_TRUE(!gcm_driver().last_account_mapping().last_message_id.empty());
+
+ GCMAccountMapper::AccountMappings mappings = GetAccounts();
+ GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin();
+ EXPECT_EQ(mapping.email, iter->email);
+ EXPECT_EQ(mapping.account_id, iter->account_id);
+ EXPECT_EQ(mapping.access_token, iter->access_token);
+ EXPECT_EQ(AccountMapping::REMOVING, iter->status);
+ EXPECT_EQ(status_change_timestamp, iter->status_change_timestamp);
+ EXPECT_EQ(gcm_driver().last_account_mapping().last_message_id,
+ iter->last_message_id);
+}
+
+// Tests that a missing token for an account will trigger removing of that
+// account. This test goes until the message is acknowledged by GCM.
+// This is a complete success scenario for account removal, and it end with
+// account mapping being completely gone.
+TEST_F(GCMAccountMapperTest, RemoveMappingMessageAcknowledged) {
+ // Start with one account that is mapped.
+ AccountMapping mapping = MakeAccountMapping(
+ kAccountId, AccountMapping::MAPPED, base::Time::Now(), std::string());
+
+ GCMAccountMapper::AccountMappings stored_mappings;
+ stored_mappings.push_back(mapping);
+ Initialize(stored_mappings);
+ gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper());
+ gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS);
+ clock()->SetNow(base::Time::Now());
+
+ mapper()->SetAccountTokens(std::vector<GCMClient::AccountTokenInfo>());
+ gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS);
+ gcm_driver().AcknowledgeSend(gcm_driver().last_message_id());
+
+ EXPECT_EQ(mapping.account_id, gcm_driver().last_removed_account_id());
+
+ GCMAccountMapper::AccountMappings mappings = GetAccounts();
+ EXPECT_TRUE(mappings.empty());
+}
+
+// Tests that account removing proceeds, when a removing message is acked after
+// Chrome was restarted.
+TEST_F(GCMAccountMapperTest, RemoveMappingMessageAckedAfterRestart) {
+ // Start with one account that is mapped.
+ AccountMapping mapping =
+ MakeAccountMapping(kAccountId, AccountMapping::REMOVING,
+ base::Time::Now(), "remove_message_id");
+
+ GCMAccountMapper::AccountMappings stored_mappings;
+ stored_mappings.push_back(mapping);
+ Initialize(stored_mappings);
+ gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper());
+
+ gcm_driver().AcknowledgeSend("remove_message_id");
+
+ EXPECT_EQ(mapping.account_id, gcm_driver().last_removed_account_id());
+
+ GCMAccountMapper::AccountMappings mappings = GetAccounts();
+ EXPECT_TRUE(mappings.empty());
+}
+
+// Tests that account removing proceeds, when a removing message is acked after
+// Chrome was restarted.
+TEST_F(GCMAccountMapperTest, RemoveMappingMessageSendError) {
+ // Start with one account that is mapped.
+ base::Time status_change_timestamp = base::Time::Now();
+ AccountMapping mapping =
+ MakeAccountMapping(kAccountId, AccountMapping::REMOVING,
+ status_change_timestamp, "remove_message_id");
+
+ GCMAccountMapper::AccountMappings stored_mappings;
+ stored_mappings.push_back(mapping);
+ Initialize(stored_mappings);
+ gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper());
+
+ clock()->SetNow(base::Time::Now());
+ gcm_driver().MessageSendError("remove_message_id");
+
+ EXPECT_TRUE(gcm_driver().last_removed_account_id().empty());
+
+ EXPECT_EQ(mapping.account_id, gcm_driver().last_account_mapping().account_id);
+ EXPECT_EQ(mapping.email, gcm_driver().last_account_mapping().email);
+ EXPECT_EQ(AccountMapping::REMOVING,
+ gcm_driver().last_account_mapping().status);
+ EXPECT_EQ(status_change_timestamp,
+ gcm_driver().last_account_mapping().status_change_timestamp);
+ // Message is not persisted, until send is completed.
+ EXPECT_TRUE(gcm_driver().last_account_mapping().last_message_id.empty());
+
+ GCMAccountMapper::AccountMappings mappings = GetAccounts();
+ GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin();
+ EXPECT_EQ(mapping.email, iter->email);
+ EXPECT_EQ(mapping.account_id, iter->account_id);
+ EXPECT_TRUE(iter->access_token.empty());
+ EXPECT_EQ(AccountMapping::REMOVING, iter->status);
+ EXPECT_EQ(status_change_timestamp, iter->status_change_timestamp);
+ EXPECT_TRUE(iter->last_message_id.empty());
+}
+
+// Tests that, if a new token arrives when the adding message is in progress
+// no new message is sent and account mapper still waits for the first one to
+// complete.
+TEST_F(GCMAccountMapperTest, TokenIsRefreshedWhenAdding) {
+ Initialize(GCMAccountMapper::AccountMappings());
+ gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS);
+
+ clock()->SetNow(base::Time::Now());
+ std::vector<GCMClient::AccountTokenInfo> account_tokens;
+ GCMClient::AccountTokenInfo account_token = MakeAccountTokenInfo(kAccountId);
+ account_tokens.push_back(account_token);
+ mapper()->SetAccountTokens(account_tokens);
+ DCHECK_EQ(CustomFakeGCMDriver::SEND_STARTED, gcm_driver().last_action());
+
+ clock()->SetNow(base::Time::Now());
+ gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS);
+ DCHECK_EQ(CustomFakeGCMDriver::SEND_FINISHED, gcm_driver().last_action());
+
+ // Providing another token and clearing status.
+ gcm_driver().Clear();
+ mapper()->SetAccountTokens(account_tokens);
+ DCHECK_EQ(CustomFakeGCMDriver::NONE, gcm_driver().last_action());
+}
+
+// Tests that, if a new token arrives when a removing message is in progress
+// a new adding message is sent and while account mapping status is changed to
+// mapped. If the original Removing message arrives it is discarded.
+TEST_F(GCMAccountMapperTest, TokenIsRefreshedWhenRemoving) {
+ // Start with one account that is mapped.
+ AccountMapping mapping = MakeAccountMapping(
+ kAccountId, AccountMapping::MAPPED, base::Time::Now(), std::string());
+
+ GCMAccountMapper::AccountMappings stored_mappings;
+ stored_mappings.push_back(mapping);
+ Initialize(stored_mappings);
+ gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS);
+ clock()->SetNow(base::Time::Now());
+
+ // Remove the token to trigger a remove message to be sent
+ mapper()->SetAccountTokens(std::vector<GCMClient::AccountTokenInfo>());
+ EXPECT_EQ(CustomFakeGCMDriver::SEND_STARTED, gcm_driver().last_action());
+ gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS);
+ EXPECT_EQ(CustomFakeGCMDriver::SEND_FINISHED, gcm_driver().last_action());
+
+ std::string remove_message_id = gcm_driver().last_message_id();
+ gcm_driver().Clear();
+
+ // The account mapping for acc_id is now in status REMOVING.
+ // Adding the token for that account.
+ clock()->SetNow(base::Time::Now());
+ std::vector<GCMClient::AccountTokenInfo> account_tokens;
+ GCMClient::AccountTokenInfo account_token = MakeAccountTokenInfo(kAccountId);
+ account_tokens.push_back(account_token);
+ mapper()->SetAccountTokens(account_tokens);
+ DCHECK_EQ(CustomFakeGCMDriver::SEND_STARTED, gcm_driver().last_action());
+ gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS);
+ EXPECT_EQ(CustomFakeGCMDriver::SEND_FINISHED, gcm_driver().last_action());
+
+ std::string add_message_id = gcm_driver().last_message_id();
+
+ // A remove message confirmation arrives now, but should be ignored.
+ gcm_driver().AcknowledgeSend(remove_message_id);
+
+ GCMAccountMapper::AccountMappings mappings = GetAccounts();
+ GCMAccountMapper::AccountMappings::const_iterator iter = mappings.begin();
+ EXPECT_EQ(mapping.email, iter->email);
+ EXPECT_EQ(mapping.account_id, iter->account_id);
+ EXPECT_FALSE(iter->access_token.empty());
+ EXPECT_EQ(AccountMapping::MAPPED, iter->status);
+ // Status change timestamp is set to very long time ago, to make sure the next
+ // round of mapping picks it up.
+ EXPECT_EQ(base::Time(), iter->status_change_timestamp);
+ EXPECT_EQ(add_message_id, iter->last_message_id);
+}
+
+// Tests adding/removing works for multiple accounts, after a restart and when
+// tokens are periodically delierverd.
+TEST_F(GCMAccountMapperTest, MultipleAccountMappings) {
+ clock()->SetNow(base::Time::Now());
+ base::Time half_hour_ago = clock()->Now() - base::Minutes(30);
+ GCMAccountMapper::AccountMappings stored_mappings;
+ stored_mappings.push_back(MakeAccountMapping(
+ kAccountId, AccountMapping::ADDING, half_hour_ago, "acc_id_msg"));
+ stored_mappings.push_back(MakeAccountMapping(
+ kAccountId1, AccountMapping::MAPPED, half_hour_ago, "acc_id_1_msg"));
+ stored_mappings.push_back(MakeAccountMapping(
+ kAccountId2, AccountMapping::REMOVING, half_hour_ago, "acc_id_2_msg"));
+
+ Initialize(stored_mappings);
+ gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper());
+ gcm_driver().CompleteRegister(kRegistrationId, GCMClient::SUCCESS);
+
+ GCMAccountMapper::AccountMappings expected_mappings(stored_mappings);
+
+ // Finish messages after a restart.
+ clock()->SetNow(base::Time::Now());
+ gcm_driver().AcknowledgeSend(expected_mappings[0].last_message_id);
+ expected_mappings[0].status_change_timestamp = clock()->Now();
+ expected_mappings[0].status = AccountMapping::MAPPED;
+ expected_mappings[0].last_message_id.clear();
+
+ clock()->SetNow(base::Time::Now());
+ gcm_driver().AcknowledgeSend(expected_mappings[1].last_message_id);
+ expected_mappings[1].status_change_timestamp = clock()->Now();
+ expected_mappings[1].status = AccountMapping::MAPPED;
+ expected_mappings[1].last_message_id.clear();
+
+ // Upon success last element is removed.
+ clock()->SetNow(base::Time::Now());
+ gcm_driver().AcknowledgeSend(expected_mappings[2].last_message_id);
+ expected_mappings.pop_back();
+
+ VerifyMappings(expected_mappings, GetAccounts(), "Step 1, After restart");
+
+ // One of accounts gets removed.
+ std::vector<GCMClient::AccountTokenInfo> account_tokens;
+ account_tokens.push_back(MakeAccountTokenInfo(kAccountId));
+
+ // Advance a day to make sure existing mappings will be reported.
+ clock()->SetNow(clock()->Now() + base::Days(1));
+ mapper()->SetAccountTokens(account_tokens);
+
+ expected_mappings[0].status = AccountMapping::MAPPED;
+ expected_mappings[1].status = AccountMapping::REMOVING;
+ expected_mappings[1].status_change_timestamp = clock()->Now();
+
+ gcm_driver().CompleteSendAllMessages();
+
+ VerifyMappings(
+ expected_mappings, GetAccounts(), "Step 2, One account is being removed");
+
+ clock()->SetNow(clock()->Now() + base::Seconds(5));
+ gcm_driver().AcknowledgeSendAllMessages();
+
+ expected_mappings[0].status_change_timestamp = clock()->Now();
+ expected_mappings.pop_back();
+
+ VerifyMappings(
+ expected_mappings, GetAccounts(), "Step 3, Removing completed");
+
+ account_tokens.clear();
+ account_tokens.push_back(MakeAccountTokenInfo(kAccountId));
+ account_tokens.push_back(MakeAccountTokenInfo(kAccountId3));
+ account_tokens.push_back(MakeAccountTokenInfo(kAccountId4));
+
+ // Advance a day to make sure existing mappings will be reported.
+ clock()->SetNow(clock()->Now() + base::Days(1));
+ mapper()->SetAccountTokens(account_tokens);
+
+ // Mapping from acc_id_0 still in position 0
+ expected_mappings.push_back(MakeAccountMapping(
+ kAccountId3, AccountMapping::NEW, base::Time(), std::string()));
+ expected_mappings.push_back(MakeAccountMapping(
+ kAccountId4, AccountMapping::NEW, base::Time(), std::string()));
+
+ VerifyMappings(expected_mappings, GetAccounts(), "Step 4, Two new accounts");
+
+ clock()->SetNow(clock()->Now() + base::Seconds(1));
+ gcm_driver().CompleteSendAllMessages();
+
+ expected_mappings[1].status = AccountMapping::ADDING;
+ expected_mappings[1].status_change_timestamp = clock()->Now();
+ expected_mappings[2].status = AccountMapping::ADDING;
+ expected_mappings[2].status_change_timestamp = clock()->Now();
+
+ VerifyMappings(
+ expected_mappings, GetAccounts(), "Step 5, Two accounts being added");
+
+ clock()->SetNow(clock()->Now() + base::Seconds(5));
+ gcm_driver().AcknowledgeSendAllMessages();
+
+ expected_mappings[0].status_change_timestamp = clock()->Now();
+ expected_mappings[1].status_change_timestamp = clock()->Now();
+ expected_mappings[1].status = AccountMapping::MAPPED;
+ expected_mappings[2].status_change_timestamp = clock()->Now();
+ expected_mappings[2].status = AccountMapping::MAPPED;
+
+ VerifyMappings(
+ expected_mappings, GetAccounts(), "Step 6, Three mapped accounts");
+}
+
+TEST_F(GCMAccountMapperTest, DispatchMessageSentToGaiaID) {
+ Initialize(GCMAccountMapper::AccountMappings());
+ gcm_driver().AddAppHandler(kGCMAccountMapperAppId, mapper());
+ IncomingMessage message;
+ message.data[kEmbeddedAppIdKey] = kTestAppId;
+ message.data[kTestDataKey] = kTestDataValue;
+ message.collapse_key = kTestCollapseKey;
+ message.sender_id = kTestSenderId;
+ mapper()->OnMessage(kGCMAccountMapperAppId, message);
+
+ EXPECT_EQ(kTestAppId, last_received_app_id());
+ EXPECT_EQ(1UL, last_received_message().data.size());
+ auto it = last_received_message().data.find(kTestDataKey);
+ EXPECT_TRUE(it != last_received_message().data.end());
+ EXPECT_EQ(kTestDataValue, it->second);
+ EXPECT_EQ(kTestCollapseKey, last_received_message().collapse_key);
+ EXPECT_EQ(kTestSenderId, last_received_message().sender_id);
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_account_tracker.cc b/chromium/components/gcm_driver/gcm_account_tracker.cc
new file mode 100644
index 00000000000..1fed7bf7d6e
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_account_tracker.cc
@@ -0,0 +1,343 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/gcm_account_tracker.h"
+
+#include <stdint.h>
+
+#include <algorithm>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/location.h"
+#include "base/task/single_thread_task_runner.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/time/time.h"
+#include "components/gcm_driver/gcm_driver.h"
+#include "components/signin/public/identity_manager/access_token_fetcher.h"
+#include "components/signin/public/identity_manager/access_token_info.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/signin/public/identity_manager/scope_set.h"
+#include "google_apis/gaia/gaia_constants.h"
+#include "google_apis/gaia/google_service_auth_error.h"
+#include "net/base/ip_endpoint.h"
+
+namespace gcm {
+
+namespace {
+
+// Name of the GCM account tracker for fetching access tokens.
+const char kGCMAccountTrackerName[] = "gcm_account_tracker";
+// Minimum token validity when sending to GCM groups server.
+const int64_t kMinimumTokenValidityMs = 500;
+// Token reporting interval, when no account changes are detected.
+const int64_t kTokenReportingIntervalMs =
+ 12 * 60 * 60 * 1000; // 12 hours in ms.
+
+} // namespace
+
+GCMAccountTracker::AccountInfo::AccountInfo(const std::string& email,
+ AccountState state)
+ : email(email), state(state) {
+}
+
+GCMAccountTracker::AccountInfo::~AccountInfo() {
+}
+
+GCMAccountTracker::GCMAccountTracker(
+ std::unique_ptr<AccountTracker> account_tracker,
+ signin::IdentityManager* identity_manager,
+ GCMDriver* driver)
+ : account_tracker_(account_tracker.release()),
+ identity_manager_(identity_manager),
+ driver_(driver),
+ shutdown_called_(false) {}
+
+GCMAccountTracker::~GCMAccountTracker() {
+ DCHECK(shutdown_called_);
+}
+
+void GCMAccountTracker::Shutdown() {
+ shutdown_called_ = true;
+ driver_->RemoveConnectionObserver(this);
+ account_tracker_->RemoveObserver(this);
+ account_tracker_->Shutdown();
+}
+
+void GCMAccountTracker::Start() {
+ DCHECK(!shutdown_called_);
+ account_tracker_->AddObserver(this);
+ driver_->AddConnectionObserver(this);
+
+ std::vector<CoreAccountInfo> accounts = account_tracker_->GetAccounts();
+ for (std::vector<CoreAccountInfo>::const_iterator iter = accounts.begin();
+ iter != accounts.end(); ++iter) {
+ if (!iter->email.empty()) {
+ account_infos_.insert(std::make_pair(
+ iter->account_id, AccountInfo(iter->email, TOKEN_NEEDED)));
+ }
+ }
+
+ if (IsTokenReportingRequired())
+ ReportTokens();
+ else
+ ScheduleReportTokens();
+}
+
+void GCMAccountTracker::ScheduleReportTokens() {
+ // Shortcutting here, in case GCM Driver is not yet connected. In that case
+ // reporting will be scheduled/started when the connection is made.
+ if (!driver_->IsConnected())
+ return;
+
+ DVLOG(1) << "Deferring the token reporting for: "
+ << GetTimeToNextTokenReporting().InSeconds() << " seconds.";
+
+ reporting_weak_ptr_factory_.InvalidateWeakPtrs();
+ base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
+ FROM_HERE,
+ base::BindOnce(&GCMAccountTracker::ReportTokens,
+ reporting_weak_ptr_factory_.GetWeakPtr()),
+ GetTimeToNextTokenReporting());
+}
+
+void GCMAccountTracker::OnAccountSignInChanged(const CoreAccountInfo& account,
+ bool is_signed_in) {
+ if (is_signed_in)
+ OnAccountSignedIn(account);
+ else
+ OnAccountSignedOut(account);
+}
+
+void GCMAccountTracker::OnAccessTokenFetchCompleteForAccount(
+ CoreAccountId account_id,
+ GoogleServiceAuthError error,
+ signin::AccessTokenInfo access_token_info) {
+ auto iter = account_infos_.find(account_id);
+ DCHECK(iter != account_infos_.end());
+ if (iter != account_infos_.end()) {
+ DCHECK_EQ(GETTING_TOKEN, iter->second.state);
+
+ if (error.state() == GoogleServiceAuthError::NONE) {
+ DVLOG(1) << "Get token success: " << account_id;
+
+ iter->second.state = TOKEN_PRESENT;
+ iter->second.access_token = access_token_info.token;
+ iter->second.expiration_time = access_token_info.expiration_time;
+ } else {
+ DVLOG(1) << "Get token failure: " << account_id;
+
+ // Given the fetcher has a built in retry logic, consider this situation
+ // to be invalid refresh token, that is only fixed when user signs in.
+ // Once the users signs in properly the minting will retry.
+ iter->second.access_token.clear();
+ iter->second.state = ACCOUNT_REMOVED;
+ }
+ }
+
+ pending_token_requests_.erase(account_id);
+ ReportTokens();
+}
+
+void GCMAccountTracker::OnConnected(const net::IPEndPoint& ip_endpoint) {
+ // We are sure here, that GCM is running and connected. We can start reporting
+ // tokens if reporting is due now, or schedule reporting for later.
+ if (IsTokenReportingRequired())
+ ReportTokens();
+ else
+ ScheduleReportTokens();
+}
+
+void GCMAccountTracker::OnDisconnected() {
+ // We are disconnected, so no point in trying to work with tokens.
+}
+
+void GCMAccountTracker::ReportTokens() {
+ SanitizeTokens();
+ // Make sure all tokens are valid.
+ if (IsTokenFetchingRequired()) {
+ GetAllNeededTokens();
+ return;
+ }
+
+ // Wait for all of the pending token requests from GCMAccountTracker to be
+ // done before you report the results.
+ if (!pending_token_requests_.empty()) {
+ return;
+ }
+
+ bool account_removed = false;
+ // Stop tracking the accounts, that were removed, as it will be reported to
+ // the driver.
+ for (auto iter = account_infos_.begin(); iter != account_infos_.end();) {
+ if (iter->second.state == ACCOUNT_REMOVED) {
+ account_removed = true;
+ account_infos_.erase(iter++);
+ } else {
+ ++iter;
+ }
+ }
+
+ std::vector<GCMClient::AccountTokenInfo> account_tokens;
+ for (auto iter = account_infos_.begin(); iter != account_infos_.end();
+ ++iter) {
+ if (iter->second.state == TOKEN_PRESENT) {
+ GCMClient::AccountTokenInfo token_info;
+ token_info.account_id = iter->first;
+ token_info.email = iter->second.email;
+ token_info.access_token = iter->second.access_token;
+ account_tokens.push_back(token_info);
+ } else {
+ // This should not happen, as we are making a check that there are no
+ // pending requests above, stopping tracking of removed accounts, or start
+ // fetching tokens.
+ NOTREACHED();
+ }
+ }
+
+ // Make sure that there is something to report, otherwise bail out.
+ if (!account_tokens.empty() || account_removed) {
+ DVLOG(1) << "Reporting the tokens to driver: " << account_tokens.size();
+ driver_->SetAccountTokens(account_tokens);
+ driver_->SetLastTokenFetchTime(base::Time::Now());
+ ScheduleReportTokens();
+ } else {
+ DVLOG(1) << "No tokens and nothing removed. Skipping callback.";
+ }
+}
+
+void GCMAccountTracker::SanitizeTokens() {
+ for (auto iter = account_infos_.begin(); iter != account_infos_.end();
+ ++iter) {
+ if (iter->second.state == TOKEN_PRESENT &&
+ iter->second.expiration_time <
+ base::Time::Now() + base::Milliseconds(kMinimumTokenValidityMs)) {
+ iter->second.access_token.clear();
+ iter->second.state = TOKEN_NEEDED;
+ iter->second.expiration_time = base::Time();
+ }
+ }
+}
+
+bool GCMAccountTracker::IsTokenReportingRequired() const {
+ if (GetTimeToNextTokenReporting().is_zero())
+ return true;
+
+ bool reporting_required = false;
+ for (auto iter = account_infos_.begin(); iter != account_infos_.end();
+ ++iter) {
+ if (iter->second.state == ACCOUNT_REMOVED)
+ reporting_required = true;
+ }
+
+ return reporting_required;
+}
+
+bool GCMAccountTracker::IsTokenFetchingRequired() const {
+ bool token_needed = false;
+ for (auto iter = account_infos_.begin(); iter != account_infos_.end();
+ ++iter) {
+ if (iter->second.state == TOKEN_NEEDED)
+ token_needed = true;
+ }
+
+ return token_needed;
+}
+
+base::TimeDelta GCMAccountTracker::GetTimeToNextTokenReporting() const {
+ base::TimeDelta time_till_next_reporting =
+ driver_->GetLastTokenFetchTime() +
+ base::Milliseconds(kTokenReportingIntervalMs) - base::Time::Now();
+
+ // Case when token fetching is overdue.
+ if (time_till_next_reporting.is_negative())
+ return base::TimeDelta();
+
+ // Case when calculated period is larger than expected, including the
+ // situation when the method is called before GCM driver is completely
+ // initialized.
+ if (time_till_next_reporting >
+ base::Milliseconds(kTokenReportingIntervalMs)) {
+ return base::Milliseconds(kTokenReportingIntervalMs);
+ }
+
+ return time_till_next_reporting;
+}
+
+void GCMAccountTracker::GetAllNeededTokens() {
+ // Only start fetching tokens if driver is running, they have a limited
+ // validity time and GCM connection is a good indication of network running.
+ // If the GetAllNeededTokens was called as part of periodic schedule, it may
+ // not have network. In that case the next network change will trigger token
+ // fetching.
+ if (!driver_->IsConnected())
+ return;
+
+ // Only start fetching access tokens if the user consented for sync.
+ if (!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync))
+ return;
+
+ for (auto iter = account_infos_.begin(); iter != account_infos_.end();
+ ++iter) {
+ if (iter->second.state == TOKEN_NEEDED)
+ GetToken(iter);
+ }
+}
+
+void GCMAccountTracker::GetToken(AccountInfos::iterator& account_iter) {
+ DCHECK_EQ(account_iter->second.state, TOKEN_NEEDED);
+
+ signin::ScopeSet scopes;
+ scopes.insert(GaiaConstants::kGCMGroupServerOAuth2Scope);
+ scopes.insert(GaiaConstants::kGCMCheckinServerOAuth2Scope);
+
+ // NOTE: It is safe to use base::Unretained() here as |token_fetcher| is owned
+ // by this object and guarantees that it will not invoke its callback after
+ // its destruction.
+ std::unique_ptr<signin::AccessTokenFetcher> token_fetcher =
+ identity_manager_->CreateAccessTokenFetcherForAccount(
+ account_iter->first, kGCMAccountTrackerName, scopes,
+ base::BindOnce(
+ &GCMAccountTracker::OnAccessTokenFetchCompleteForAccount,
+ base::Unretained(this), account_iter->first),
+ signin::AccessTokenFetcher::Mode::kImmediate);
+
+ DCHECK(pending_token_requests_.count(account_iter->first) == 0);
+ pending_token_requests_.emplace(account_iter->first,
+ std::move(token_fetcher));
+ account_iter->second.state = GETTING_TOKEN;
+}
+
+void GCMAccountTracker::OnAccountSignedIn(const CoreAccountInfo& account) {
+ DVLOG(1) << "Account signed in: " << account.email;
+ auto iter = account_infos_.find(account.account_id);
+ if (iter == account_infos_.end()) {
+ DCHECK(!account.email.empty());
+ account_infos_.insert(std::make_pair(
+ account.account_id, AccountInfo(account.email, TOKEN_NEEDED)));
+ } else if (iter->second.state == ACCOUNT_REMOVED) {
+ iter->second.state = TOKEN_NEEDED;
+ }
+
+ GetAllNeededTokens();
+}
+
+void GCMAccountTracker::OnAccountSignedOut(const CoreAccountInfo& account) {
+ DVLOG(1) << "Account signed out: " << account.email;
+ auto iter = account_infos_.find(account.account_id);
+ if (iter == account_infos_.end())
+ return;
+
+ iter->second.access_token.clear();
+ iter->second.state = ACCOUNT_REMOVED;
+
+ // Delete any ongoing access token request now so that if the account is later
+ // re-added and a new access token request made, we do not break this class'
+ // invariant that there is at most one ongoing access token request per
+ // account.
+ pending_token_requests_.erase(account.account_id);
+ ReportTokens();
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_account_tracker.h b/chromium/components/gcm_driver/gcm_account_tracker.h
new file mode 100644
index 00000000000..91d2fd3667d
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_account_tracker.h
@@ -0,0 +1,172 @@
+// Copyright 2014 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 COMPONENTS_GCM_DRIVER_GCM_ACCOUNT_TRACKER_H_
+#define COMPONENTS_GCM_DRIVER_GCM_ACCOUNT_TRACKER_H_
+
+#include <stddef.h>
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include "base/memory/raw_ptr.h"
+#include "components/gcm_driver/account_tracker.h"
+#include "components/gcm_driver/gcm_client.h"
+#include "components/gcm_driver/gcm_connection_observer.h"
+#include "components/signin/public/identity_manager/access_token_fetcher.h"
+
+namespace signin {
+struct AccessTokenInfo;
+class IdentityManager;
+} // namespace signin
+
+namespace base {
+class Time;
+}
+
+namespace gcm {
+
+class GCMDriver;
+
+// Class for reporting back which accounts are signed into. It is only meant to
+// be used when the user is signed into sync.
+//
+// This class makes a check for tokens periodically, to make sure the user is
+// still logged into the profile, so that in the case that the user is not, we
+// can immediately report that to the GCM and stop messages addressed to that
+// user from ever reaching Chrome.
+class GCMAccountTracker : public AccountTracker::Observer,
+ public GCMConnectionObserver {
+ public:
+ // State of the account.
+ // Allowed transitions:
+ // TOKEN_NEEDED - account info was created.
+ // TOKEN_NEEDED -> GETTING_TOKEN - access token was requested.
+ // GETTING_TOKEN -> TOKEN_NEEDED - access token fetching failed.
+ // GETTING_TOKEN -> TOKEN_PRESENT - access token fetching succeeded.
+ // GETTING_TOKEN -> ACCOUNT_REMOVED - account was removed.
+ // TOKEN_NEEDED -> ACCOUNT_REMOVED - account was removed.
+ // TOKEN_PRESENT -> ACCOUNT_REMOVED - account was removed.
+ enum AccountState {
+ TOKEN_NEEDED, // Needs a token (AccountInfo was recently created or
+ // token request failed).
+ GETTING_TOKEN, // There is a pending token request.
+ TOKEN_PRESENT, // We have a token for the account.
+ ACCOUNT_REMOVED, // Account was removed, and we didn't report it yet.
+ };
+
+ // Stores necessary account information and state of token fetching.
+ struct AccountInfo {
+ AccountInfo(const std::string& email, AccountState state);
+ ~AccountInfo();
+
+ // Email address of the tracked account.
+ std::string email;
+ // OAuth2 access token, when |state| is TOKEN_PRESENT.
+ std::string access_token;
+ // Expiration time of the access tokens.
+ base::Time expiration_time;
+ // Status of the token fetching.
+ AccountState state;
+ };
+
+ // |account_tracker| is used to deliver information about the accounts present
+ // in the browser context to |driver|.
+ GCMAccountTracker(std::unique_ptr<AccountTracker> account_tracker,
+ signin::IdentityManager* identity_manager,
+ GCMDriver* driver);
+
+ GCMAccountTracker(const GCMAccountTracker&) = delete;
+ GCMAccountTracker& operator=(const GCMAccountTracker&) = delete;
+
+ ~GCMAccountTracker() override;
+
+ // Shuts down the tracker ensuring a proper clean up. After Shutdown() is
+ // called Start() and Stop() should no longer be used. Must be called before
+ // destruction.
+ void Shutdown();
+
+ // Starts tracking accounts.
+ void Start();
+
+ // Gets the number of pending token requests. Only used for testing.
+ size_t get_pending_token_request_count() const {
+ return pending_token_requests_.size();
+ }
+
+ private:
+ friend class GCMAccountTrackerTest;
+
+ // Maps account keys to account states. Keyed by account_id as used by
+ // IdentityManager.
+ typedef std::map<CoreAccountId, AccountInfo> AccountInfos;
+
+ // AccountTracker::Observer overrides.
+ void OnAccountSignInChanged(const CoreAccountInfo& account,
+ bool is_signed_in) override;
+
+ void OnAccessTokenFetchCompleteForAccount(
+ CoreAccountId account_id,
+ GoogleServiceAuthError error,
+ signin::AccessTokenInfo access_token_info);
+
+ // GCMConnectionObserver overrides.
+ void OnConnected(const net::IPEndPoint& ip_endpoint) override;
+ void OnDisconnected() override;
+
+ // Schedules token reporting.
+ void ScheduleReportTokens();
+ // Report the list of accounts with OAuth2 tokens back using the |callback_|
+ // function. If there are token requests in progress, do nothing.
+ void ReportTokens();
+ // Verify that all of the tokens are ready to be passed down to the GCM
+ // Driver, e.g. none of them has expired or is missing. Returns true if not
+ // all tokens are valid and a fetching yet more tokens is required.
+ void SanitizeTokens();
+ // Indicates whether token reporting is required, either because it is due, or
+ // some of the accounts were removed.
+ bool IsTokenReportingRequired() const;
+ // Indicates whether there are tokens that still need fetching.
+ bool IsTokenFetchingRequired() const;
+ // Gets the time until next token reporting.
+ base::TimeDelta GetTimeToNextTokenReporting() const;
+ // Checks on all known accounts, and calls GetToken(..) for those with
+ // |state == TOKEN_NEEDED|.
+ void GetAllNeededTokens();
+ // Starts fetching the OAuth2 token for the GCM group scope.
+ void GetToken(AccountInfos::iterator& account_iter);
+
+ // Handling of actual sign in and sign out for accounts.
+ void OnAccountSignedIn(const CoreAccountInfo& account);
+ void OnAccountSignedOut(const CoreAccountInfo& account);
+
+ // Account tracker.
+ std::unique_ptr<AccountTracker> account_tracker_;
+
+ raw_ptr<signin::IdentityManager> identity_manager_;
+
+ raw_ptr<GCMDriver> driver_;
+
+ // State of the account.
+ AccountInfos account_infos_;
+
+ // Indicates whether shutdown has been called.
+ bool shutdown_called_;
+
+ // Stores the ongoing access token fetchers for deletion either upon
+ // completion or upon signout of the account for which the request is being
+ // made.
+ using AccountIDToTokenFetcherMap =
+ std::map<CoreAccountId, std::unique_ptr<signin::AccessTokenFetcher>>;
+ AccountIDToTokenFetcherMap pending_token_requests_;
+
+ // Creates weak pointers used to postpone reporting tokens. See
+ // ScheduleReportTokens.
+ base::WeakPtrFactory<GCMAccountTracker> reporting_weak_ptr_factory_{this};
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_ACCOUNT_TRACKER_H_
diff --git a/chromium/components/gcm_driver/gcm_account_tracker_unittest.cc b/chromium/components/gcm_driver/gcm_account_tracker_unittest.cc
new file mode 100644
index 00000000000..c2ccf550972
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_account_tracker_unittest.cc
@@ -0,0 +1,534 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/gcm_account_tracker.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/memory/raw_ptr.h"
+#include "base/test/task_environment.h"
+#include "build/chromeos_buildflags.h"
+#include "components/gcm_driver/fake_gcm_driver.h"
+#include "components/signin/public/identity_manager/identity_test_environment.h"
+#include "google_apis/gaia/google_service_auth_error.h"
+#include "net/base/ip_endpoint.h"
+#include "net/http/http_status_code.h"
+#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
+#include "services/network/test/test_url_loader_factory.h"
+#include "services/network/test/test_utils.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+
+const char kEmail1[] = "account_1@me.com";
+const char kEmail2[] = "account_2@me.com";
+
+std::string MakeAccessToken(const CoreAccountId& account_id) {
+ return "access_token-" + account_id.ToString();
+}
+
+GCMClient::AccountTokenInfo MakeAccountToken(const CoreAccountInfo& account) {
+ GCMClient::AccountTokenInfo token_info;
+ token_info.account_id = account.account_id;
+
+ // TODO(https://crbug.com/856170): This *should* be expected to be the email
+ // address for the given account, but there is a bug in AccountTracker that
+ // means that |token_info.email| actually gets populated with the account ID
+ // by the production code. Hence the test expectation has to match what the
+ // production code actually does :). If/when that bug gets fixed, this
+ // function should be changed to take in the email address as well as the
+ // account ID and populate this field with the email address.
+ token_info.email = account.email;
+ token_info.access_token = MakeAccessToken(account.account_id);
+ return token_info;
+}
+
+void VerifyAccountTokens(
+ const std::vector<GCMClient::AccountTokenInfo>& expected_tokens,
+ const std::vector<GCMClient::AccountTokenInfo>& actual_tokens) {
+ EXPECT_EQ(expected_tokens.size(), actual_tokens.size());
+ for (auto expected_iter = expected_tokens.begin(),
+ actual_iter = actual_tokens.begin();
+ expected_iter != expected_tokens.end() &&
+ actual_iter != actual_tokens.end();
+ ++expected_iter, ++actual_iter) {
+ EXPECT_EQ(expected_iter->account_id, actual_iter->account_id);
+ EXPECT_EQ(expected_iter->email, actual_iter->email);
+ EXPECT_EQ(expected_iter->access_token, actual_iter->access_token);
+ }
+}
+
+// This version of FakeGCMDriver is customized around handling accounts and
+// connection events for testing GCMAccountTracker.
+class CustomFakeGCMDriver : public FakeGCMDriver {
+ public:
+ CustomFakeGCMDriver();
+
+ CustomFakeGCMDriver(const CustomFakeGCMDriver&) = delete;
+ CustomFakeGCMDriver& operator=(const CustomFakeGCMDriver&) = delete;
+
+ ~CustomFakeGCMDriver() override;
+
+ // GCMDriver overrides:
+ void SetAccountTokens(
+ const std::vector<GCMClient::AccountTokenInfo>& account_tokens) override;
+ void AddConnectionObserver(GCMConnectionObserver* observer) override;
+ void RemoveConnectionObserver(GCMConnectionObserver* observer) override;
+ bool IsConnected() const override { return connected_; }
+ base::Time GetLastTokenFetchTime() override;
+ void SetLastTokenFetchTime(const base::Time& time) override;
+
+ // Test results and helpers.
+ void SetConnected(bool connected);
+ void ResetResults();
+ bool update_accounts_called() const { return update_accounts_called_; }
+ const std::vector<GCMClient::AccountTokenInfo>& accounts() const {
+ return accounts_;
+ }
+ const GCMConnectionObserver* last_connection_observer() const {
+ return last_connection_observer_;
+ }
+ const GCMConnectionObserver* last_removed_connection_observer() const {
+ return removed_connection_observer_;
+ }
+
+ private:
+ bool connected_;
+ std::vector<GCMClient::AccountTokenInfo> accounts_;
+ bool update_accounts_called_;
+ raw_ptr<GCMConnectionObserver> last_connection_observer_;
+ raw_ptr<GCMConnectionObserver> removed_connection_observer_;
+ net::IPEndPoint ip_endpoint_;
+ base::Time last_token_fetch_time_;
+};
+
+CustomFakeGCMDriver::CustomFakeGCMDriver()
+ : connected_(true),
+ update_accounts_called_(false),
+ last_connection_observer_(nullptr),
+ removed_connection_observer_(nullptr) {}
+
+CustomFakeGCMDriver::~CustomFakeGCMDriver() {
+}
+
+void CustomFakeGCMDriver::SetAccountTokens(
+ const std::vector<GCMClient::AccountTokenInfo>& accounts) {
+ update_accounts_called_ = true;
+ accounts_ = accounts;
+}
+
+void CustomFakeGCMDriver::AddConnectionObserver(
+ GCMConnectionObserver* observer) {
+ last_connection_observer_ = observer;
+}
+
+void CustomFakeGCMDriver::RemoveConnectionObserver(
+ GCMConnectionObserver* observer) {
+ removed_connection_observer_ = observer;
+}
+
+void CustomFakeGCMDriver::SetConnected(bool connected) {
+ connected_ = connected;
+ if (connected && last_connection_observer_)
+ last_connection_observer_->OnConnected(ip_endpoint_);
+}
+
+void CustomFakeGCMDriver::ResetResults() {
+ accounts_.clear();
+ update_accounts_called_ = false;
+ last_connection_observer_ = nullptr;
+ removed_connection_observer_ = nullptr;
+}
+
+
+base::Time CustomFakeGCMDriver::GetLastTokenFetchTime() {
+ return last_token_fetch_time_;
+}
+
+void CustomFakeGCMDriver::SetLastTokenFetchTime(const base::Time& time) {
+ last_token_fetch_time_ = time;
+}
+
+} // namespace
+
+class GCMAccountTrackerTest : public testing::Test {
+ public:
+ GCMAccountTrackerTest();
+ ~GCMAccountTrackerTest() override;
+
+ // Helpers to pass fake info to the tracker.
+ CoreAccountInfo AddAccount(const std::string& email);
+ CoreAccountInfo SetPrimaryAccount(const std::string& email);
+ void ClearPrimaryAccount();
+ void RemoveAccount(const CoreAccountId& account_id);
+
+ // Helpers for dealing with OAuth2 access token requests.
+ void IssueAccessToken(const CoreAccountId& account_id);
+ void IssueExpiredAccessToken(const CoreAccountId& account_id);
+ void IssueError(const CoreAccountId& account_id);
+
+ // Accessors to account tracker and gcm driver.
+ GCMAccountTracker* tracker() { return tracker_.get(); }
+ CustomFakeGCMDriver* driver() { return &driver_; }
+
+ // Accessors to private methods of account tracker.
+ bool IsFetchingRequired() const;
+ bool IsTokenReportingRequired() const;
+ base::TimeDelta GetTimeToNextTokenReporting() const;
+
+ network::TestURLLoaderFactory* test_url_loader_factory() {
+ return &test_url_loader_factory_;
+ }
+
+ private:
+ CustomFakeGCMDriver driver_;
+
+ base::test::SingleThreadTaskEnvironment task_environment_;
+ network::TestURLLoaderFactory test_url_loader_factory_;
+ signin::IdentityTestEnvironment identity_test_env_;
+
+ std::unique_ptr<GCMAccountTracker> tracker_;
+};
+
+GCMAccountTrackerTest::GCMAccountTrackerTest() {
+ std::unique_ptr<AccountTracker> gaia_account_tracker(
+ new AccountTracker(identity_test_env_.identity_manager()));
+
+ tracker_ = std::make_unique<GCMAccountTracker>(
+ std::move(gaia_account_tracker), identity_test_env_.identity_manager(),
+ &driver_);
+}
+
+GCMAccountTrackerTest::~GCMAccountTrackerTest() {
+ if (tracker_)
+ tracker_->Shutdown();
+}
+
+CoreAccountInfo GCMAccountTrackerTest::AddAccount(const std::string& email) {
+ return identity_test_env_.MakeAccountAvailable(email);
+}
+
+CoreAccountInfo GCMAccountTrackerTest::SetPrimaryAccount(
+ const std::string& email) {
+ // NOTE: Setting of the primary account info must be done first on ChromeOS
+ // to ensure that AccountTracker and GCMAccountTracker respond as expected
+ // when the token is added to the token service.
+ // TODO(blundell): On non-ChromeOS, it would be good to add tests wherein
+ // setting of the primary account is done afterward to check that the flow
+ // that ensues from the GoogleSigninSucceeded callback firing works as
+ // expected.
+ return identity_test_env_.MakePrimaryAccountAvailable(
+ email, signin::ConsentLevel::kSync);
+}
+
+void GCMAccountTrackerTest::ClearPrimaryAccount() {
+ identity_test_env_.ClearPrimaryAccount();
+}
+
+void GCMAccountTrackerTest::RemoveAccount(const CoreAccountId& account_id) {
+ identity_test_env_.RemoveRefreshTokenForAccount(account_id);
+}
+
+void GCMAccountTrackerTest::IssueAccessToken(const CoreAccountId& account_id) {
+ identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
+ account_id, MakeAccessToken(account_id), base::Time::Max());
+}
+
+void GCMAccountTrackerTest::IssueExpiredAccessToken(
+ const CoreAccountId& account_id) {
+ identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
+ account_id, MakeAccessToken(account_id), base::Time::Now());
+}
+
+void GCMAccountTrackerTest::IssueError(const CoreAccountId& account_id) {
+ identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithError(
+ account_id,
+ GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE));
+}
+
+bool GCMAccountTrackerTest::IsFetchingRequired() const {
+ return tracker_->IsTokenFetchingRequired();
+}
+
+bool GCMAccountTrackerTest::IsTokenReportingRequired() const {
+ return tracker_->IsTokenReportingRequired();
+}
+
+base::TimeDelta GCMAccountTrackerTest::GetTimeToNextTokenReporting() const {
+ return tracker_->GetTimeToNextTokenReporting();
+}
+
+TEST_F(GCMAccountTrackerTest, NoAccounts) {
+ EXPECT_FALSE(driver()->update_accounts_called());
+ tracker()->Start();
+ // Callback should not be called if there where no accounts provided.
+ EXPECT_FALSE(driver()->update_accounts_called());
+ EXPECT_TRUE(driver()->accounts().empty());
+}
+
+// Verifies that callback is called after a token is issued for a single account
+// with a specific scope. In this scenario, the underlying account tracker is
+// still working when the CompleteCollectingTokens is called for the first time.
+TEST_F(GCMAccountTrackerTest, SingleAccount) {
+ CoreAccountInfo account1 = SetPrimaryAccount(kEmail1);
+
+ tracker()->Start();
+ EXPECT_FALSE(driver()->update_accounts_called());
+
+ IssueAccessToken(account1.account_id);
+ EXPECT_TRUE(driver()->update_accounts_called());
+
+ std::vector<GCMClient::AccountTokenInfo> expected_accounts;
+ expected_accounts.push_back(MakeAccountToken(account1));
+ VerifyAccountTokens(expected_accounts, driver()->accounts());
+}
+
+TEST_F(GCMAccountTrackerTest, MultipleAccounts) {
+ CoreAccountInfo account1 = SetPrimaryAccount(kEmail1);
+ CoreAccountInfo account2 = AddAccount(kEmail2);
+
+ tracker()->Start();
+ EXPECT_FALSE(driver()->update_accounts_called());
+
+ IssueAccessToken(account1.account_id);
+ EXPECT_FALSE(driver()->update_accounts_called());
+
+ IssueAccessToken(account2.account_id);
+ EXPECT_TRUE(driver()->update_accounts_called());
+
+ std::vector<GCMClient::AccountTokenInfo> expected_accounts;
+ expected_accounts.push_back(MakeAccountToken(account1));
+ expected_accounts.push_back(MakeAccountToken(account2));
+ VerifyAccountTokens(expected_accounts, driver()->accounts());
+}
+
+TEST_F(GCMAccountTrackerTest, AccountAdded) {
+ tracker()->Start();
+ driver()->ResetResults();
+
+ CoreAccountInfo account1 = SetPrimaryAccount(kEmail1);
+ EXPECT_FALSE(driver()->update_accounts_called());
+
+ IssueAccessToken(account1.account_id);
+ EXPECT_TRUE(driver()->update_accounts_called());
+
+ std::vector<GCMClient::AccountTokenInfo> expected_accounts;
+ expected_accounts.push_back(MakeAccountToken(account1));
+ VerifyAccountTokens(expected_accounts, driver()->accounts());
+}
+
+TEST_F(GCMAccountTrackerTest, AccountRemoved) {
+ CoreAccountInfo account1 = SetPrimaryAccount(kEmail1);
+ CoreAccountInfo account2 = AddAccount(kEmail2);
+
+ tracker()->Start();
+ IssueAccessToken(account1.account_id);
+ IssueAccessToken(account2.account_id);
+ EXPECT_TRUE(driver()->update_accounts_called());
+
+ driver()->ResetResults();
+ EXPECT_FALSE(driver()->update_accounts_called());
+
+ RemoveAccount(account2.account_id);
+ EXPECT_TRUE(driver()->update_accounts_called());
+
+ std::vector<GCMClient::AccountTokenInfo> expected_accounts;
+ expected_accounts.push_back(MakeAccountToken(account1));
+ VerifyAccountTokens(expected_accounts, driver()->accounts());
+}
+
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+// Tests that clearing the primary account when having multiple accounts
+// does not crash the application.
+// Regression test for crbug.com/1234406
+TEST_F(GCMAccountTrackerTest, AccountRemovedWithoutSyncConsentNoCrash) {
+ CoreAccountInfo account1 = SetPrimaryAccount(kEmail1);
+ CoreAccountInfo account2 = AddAccount(kEmail2);
+
+ // Set last fetch time to now so that access token fetch is not required
+ // but not started.
+ driver()->SetLastTokenFetchTime(base::Time::Now());
+ tracker()->Start();
+ EXPECT_FALSE(driver()->update_accounts_called());
+
+ // Reset the last fetch time to verify that clearing the primary account
+ // will not trigger a token fetch.
+ driver()->SetLastTokenFetchTime(base::Time());
+ EXPECT_EQ(base::TimeDelta(), GetTimeToNextTokenReporting());
+ ClearPrimaryAccount();
+ EXPECT_TRUE(driver()->update_accounts_called());
+}
+#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
+
+TEST_F(GCMAccountTrackerTest, GetTokenFailed) {
+ CoreAccountInfo account1 = SetPrimaryAccount(kEmail1);
+ CoreAccountInfo account2 = AddAccount(kEmail2);
+
+ tracker()->Start();
+ IssueAccessToken(account1.account_id);
+ EXPECT_FALSE(driver()->update_accounts_called());
+
+ IssueError(account2.account_id);
+
+ // Failed token is not retried any more. Account marked as removed.
+ EXPECT_EQ(0UL, tracker()->get_pending_token_request_count());
+ EXPECT_TRUE(driver()->update_accounts_called());
+
+ std::vector<GCMClient::AccountTokenInfo> expected_accounts;
+ expected_accounts.push_back(MakeAccountToken(account1));
+ VerifyAccountTokens(expected_accounts, driver()->accounts());
+}
+
+TEST_F(GCMAccountTrackerTest, GetTokenFailedAccountRemoved) {
+ CoreAccountInfo account1 = SetPrimaryAccount(kEmail1);
+ CoreAccountInfo account2 = AddAccount(kEmail2);
+
+ tracker()->Start();
+ IssueAccessToken(account1.account_id);
+
+ driver()->ResetResults();
+ RemoveAccount(account2.account_id);
+ IssueError(account2.account_id);
+
+ EXPECT_TRUE(driver()->update_accounts_called());
+
+ std::vector<GCMClient::AccountTokenInfo> expected_accounts;
+ expected_accounts.push_back(MakeAccountToken(account1));
+ VerifyAccountTokens(expected_accounts, driver()->accounts());
+}
+
+TEST_F(GCMAccountTrackerTest, AccountRemovedWhileRequestsPending) {
+ CoreAccountInfo account1 = SetPrimaryAccount(kEmail1);
+ CoreAccountInfo account2 = AddAccount(kEmail2);
+
+ tracker()->Start();
+ IssueAccessToken(account1.account_id);
+ EXPECT_FALSE(driver()->update_accounts_called());
+
+ RemoveAccount(account2.account_id);
+ IssueAccessToken(account2.account_id);
+ EXPECT_TRUE(driver()->update_accounts_called());
+
+ std::vector<GCMClient::AccountTokenInfo> expected_accounts;
+ expected_accounts.push_back(MakeAccountToken(account1));
+ VerifyAccountTokens(expected_accounts, driver()->accounts());
+}
+
+// Makes sure that tracker observes GCM connection when running.
+TEST_F(GCMAccountTrackerTest, TrackerObservesConnection) {
+ EXPECT_EQ(nullptr, driver()->last_connection_observer());
+ tracker()->Start();
+ EXPECT_EQ(tracker(), driver()->last_connection_observer());
+ tracker()->Shutdown();
+ EXPECT_EQ(tracker(), driver()->last_removed_connection_observer());
+}
+
+// Makes sure that token fetching happens only after connection is established.
+TEST_F(GCMAccountTrackerTest, PostponeTokenFetchingUntilConnected) {
+ driver()->SetConnected(false);
+ CoreAccountInfo account1 = SetPrimaryAccount(kEmail1);
+ tracker()->Start();
+
+ EXPECT_EQ(0UL, tracker()->get_pending_token_request_count());
+ driver()->SetConnected(true);
+
+ EXPECT_EQ(1UL, tracker()->get_pending_token_request_count());
+}
+
+TEST_F(GCMAccountTrackerTest, InvalidateExpiredTokens) {
+ CoreAccountInfo account1 = SetPrimaryAccount(kEmail1);
+ CoreAccountInfo account2 = AddAccount(kEmail2);
+ tracker()->Start();
+
+ EXPECT_EQ(2UL, tracker()->get_pending_token_request_count());
+
+ IssueExpiredAccessToken(account1.account_id);
+ IssueAccessToken(account2.account_id);
+ // Because the first token is expired, we expect the sanitize to kick in and
+ // clean it up before the SetAccessToken is called. This also means a new
+ // token request will be issued
+ EXPECT_FALSE(driver()->update_accounts_called());
+ EXPECT_EQ(1UL, tracker()->get_pending_token_request_count());
+}
+
+// Testing for whether there are still more tokens to be fetched. Typically the
+// need for token fetching triggers immediate request, unless there is no
+// connection, that is why connection is set on and off in this test.
+TEST_F(GCMAccountTrackerTest, IsTokenFetchingRequired) {
+ tracker()->Start();
+ driver()->SetConnected(false);
+ EXPECT_FALSE(IsFetchingRequired());
+ CoreAccountInfo account1 = SetPrimaryAccount(kEmail1);
+ EXPECT_TRUE(IsFetchingRequired());
+
+ driver()->SetConnected(true);
+ EXPECT_FALSE(IsFetchingRequired()); // Indicates that fetching has started.
+ IssueAccessToken(account1.account_id);
+ EXPECT_FALSE(IsFetchingRequired());
+
+ CoreAccountInfo account2 = AddAccount(kEmail2);
+ EXPECT_FALSE(IsFetchingRequired()); // Indicates that fetching has started.
+
+ // Disconnect the driver again so that the access token request being
+ // fulfilled doesn't immediately cause another access token request (which
+ // then would cause IsFetchingRequired() to be false, preventing us from
+ // distinguishing this case from the case where IsFetchingRequired() is false
+ // because GCMAccountTracker didn't detect that a new access token needs to be
+ // fetched).
+ driver()->SetConnected(false);
+ IssueExpiredAccessToken(account2.account_id);
+
+ // Make sure that if the token was expired it is marked as being needed again.
+ EXPECT_TRUE(IsFetchingRequired());
+}
+
+// Tests what is the expected time to the next token fetching.
+TEST_F(GCMAccountTrackerTest, GetTimeToNextTokenReporting) {
+ tracker()->Start();
+ // At this point the last token fetch time is never.
+ EXPECT_EQ(base::TimeDelta(), GetTimeToNextTokenReporting());
+
+ // Regular case. The tokens have been just reported.
+ driver()->SetLastTokenFetchTime(base::Time::Now());
+ EXPECT_TRUE(GetTimeToNextTokenReporting() <= base::Seconds(12 * 60 * 60));
+
+ // A case when gcm driver is not yet initialized.
+ driver()->SetLastTokenFetchTime(base::Time::Max());
+ EXPECT_EQ(base::Seconds(12 * 60 * 60), GetTimeToNextTokenReporting());
+
+ // A case when token reporting calculation is expected to result in more than
+ // 12 hours, in which case we expect exactly 12 hours.
+ driver()->SetLastTokenFetchTime(base::Time::Now() + base::Days(2));
+ EXPECT_EQ(base::Seconds(12 * 60 * 60), GetTimeToNextTokenReporting());
+}
+
+// Tests conditions when token reporting is required.
+TEST_F(GCMAccountTrackerTest, IsTokenReportingRequired) {
+ tracker()->Start();
+ // Required because it is overdue.
+ EXPECT_TRUE(IsTokenReportingRequired());
+
+ // Not required because it just happened.
+ driver()->SetLastTokenFetchTime(base::Time::Now());
+ EXPECT_FALSE(IsTokenReportingRequired());
+
+ CoreAccountInfo account1 = SetPrimaryAccount(kEmail1);
+ IssueAccessToken(account1.account_id);
+ driver()->ResetResults();
+ // Reporting was triggered, which means testing for required will give false,
+ // but we have the update call.
+ RemoveAccount(account1.account_id);
+ EXPECT_TRUE(driver()->update_accounts_called());
+ EXPECT_FALSE(IsTokenReportingRequired());
+}
+
+// TODO(fgorski): Add test for adding account after removal >> make sure it does
+// not mark removal.
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_activity.cc b/chromium/components/gcm_driver/gcm_activity.cc
new file mode 100644
index 00000000000..bee57d4b988
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_activity.cc
@@ -0,0 +1,62 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/gcm_activity.h"
+
+namespace gcm {
+
+Activity::Activity()
+ : time(base::Time::Now()) {
+}
+
+Activity::~Activity() {
+}
+
+CheckinActivity::CheckinActivity() {
+}
+
+CheckinActivity::~CheckinActivity() {
+}
+
+ConnectionActivity::ConnectionActivity() {
+}
+
+ConnectionActivity::~ConnectionActivity() {
+}
+
+RegistrationActivity::RegistrationActivity() {
+}
+
+RegistrationActivity::~RegistrationActivity() {
+}
+
+ReceivingActivity::ReceivingActivity()
+ : message_byte_size(0) {
+}
+
+ReceivingActivity::~ReceivingActivity() {
+}
+
+SendingActivity::SendingActivity() {
+}
+
+SendingActivity::~SendingActivity() {
+}
+
+DecryptionFailureActivity::DecryptionFailureActivity() {
+}
+
+DecryptionFailureActivity::~DecryptionFailureActivity() {
+}
+
+RecordedActivities::RecordedActivities() {
+}
+
+RecordedActivities::RecordedActivities(const RecordedActivities& other) =
+ default;
+
+RecordedActivities::~RecordedActivities() {
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_activity.h b/chromium/components/gcm_driver/gcm_activity.h
new file mode 100644
index 00000000000..80c9d0316f9
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_activity.h
@@ -0,0 +1,90 @@
+// Copyright 2014 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 COMPONENTS_GCM_DRIVER_GCM_ACTIVITY_H_
+#define COMPONENTS_GCM_DRIVER_GCM_ACTIVITY_H_
+
+#include <string>
+#include <vector>
+
+#include "base/time/time.h"
+
+namespace gcm {
+
+// Contains data that are common to all activity kinds below.
+struct Activity {
+ Activity();
+ virtual ~Activity();
+
+ base::Time time;
+ std::string event; // A short description of the event.
+ std::string details; // Any additional detail about the event.
+};
+
+// Contains relevant data of a connection activity.
+struct ConnectionActivity : Activity {
+ ConnectionActivity();
+ ~ConnectionActivity() override;
+};
+
+// Contains relevant data of a check-in activity.
+struct CheckinActivity : Activity {
+ CheckinActivity();
+ ~CheckinActivity() override;
+};
+
+// Contains relevant data of a registration/unregistration step.
+struct RegistrationActivity : Activity {
+ RegistrationActivity();
+ ~RegistrationActivity() override;
+
+ std::string app_id;
+ // For GCM, comma separated sender ids. For Instance ID, authorized entity.
+ std::string source;
+};
+
+// Contains relevant data of a message receiving event.
+struct ReceivingActivity : Activity {
+ ReceivingActivity();
+ ~ReceivingActivity() override;
+
+ std::string app_id;
+ std::string from;
+ int message_byte_size;
+};
+
+// Contains relevant data of a send-message step.
+struct SendingActivity : Activity {
+ SendingActivity();
+ ~SendingActivity() override;
+
+ std::string app_id;
+ std::string receiver_id;
+ std::string message_id;
+};
+
+// Contains relevant data of a message decryption failure.
+struct DecryptionFailureActivity : Activity {
+ DecryptionFailureActivity();
+ ~DecryptionFailureActivity() override;
+
+ std::string app_id;
+};
+
+struct RecordedActivities {
+ RecordedActivities();
+ RecordedActivities(const RecordedActivities& other);
+ virtual ~RecordedActivities();
+
+ std::vector<CheckinActivity> checkin_activities;
+ std::vector<ConnectionActivity> connection_activities;
+ std::vector<RegistrationActivity> registration_activities;
+ std::vector<ReceivingActivity> receiving_activities;
+ std::vector<SendingActivity> sending_activities;
+ std::vector<DecryptionFailureActivity> decryption_failure_activities;
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_ACTIVITY_H_
diff --git a/chromium/components/gcm_driver/gcm_app_handler.cc b/chromium/components/gcm_driver/gcm_app_handler.cc
new file mode 100644
index 00000000000..c8827995d44
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_app_handler.cc
@@ -0,0 +1,21 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/gcm_app_handler.h"
+
+namespace gcm {
+
+GCMAppHandler::GCMAppHandler() = default;
+GCMAppHandler::~GCMAppHandler() = default;
+
+void GCMAppHandler::OnMessageDecryptionFailed(
+ const std::string& app_id,
+ const std::string& message_id,
+ const std::string& error_message) {}
+
+bool GCMAppHandler::CanHandle(const std::string& app_id) const {
+ return false;
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_app_handler.h b/chromium/components/gcm_driver/gcm_app_handler.h
new file mode 100644
index 00000000000..676eeea8960
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_app_handler.h
@@ -0,0 +1,67 @@
+// Copyright (c) 2014 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 COMPONENTS_GCM_DRIVER_GCM_APP_HANDLER_H_
+#define COMPONENTS_GCM_DRIVER_GCM_APP_HANDLER_H_
+
+#include <string>
+
+#include "components/gcm_driver/gcm_client.h"
+
+namespace gcm {
+
+// Defines the interface to provide handling and event routing logic for a given
+// app.
+class GCMAppHandler {
+ public:
+ GCMAppHandler();
+ virtual ~GCMAppHandler();
+
+ // Called to do all the cleanup when GCM is shutting down.
+ // In the case that multiple apps share the same app handler, it should be
+ // make safe for ShutdownHandler to be called multiple times.
+ virtual void ShutdownHandler() = 0;
+
+ // Called when the GCM store is reset (e.g. due to corruption), which changes
+ // the device ID, invalidating all prior registrations. Any stored state
+ // related to GCM registrations or InstanceIDs should be deleted. This should
+ // only be considered a defense in depth, as this method will not be called if
+ // the store is reset before this app handler is registered; hence it is
+ // recommended to regularly revalidate any stored registrations/InstanceIDs.
+ // TODO(johnme): GCMDriver doesn't yet provide an API for revalidating them.
+ virtual void OnStoreReset() = 0;
+
+ // Called when a GCM message has been received.
+ virtual void OnMessage(const std::string& app_id,
+ const IncomingMessage& message) = 0;
+
+ // Called when some GCM messages have been deleted from the server.
+ virtual void OnMessagesDeleted(const std::string& app_id) = 0;
+
+ // Called when a GCM message failed to be delivered.
+ virtual void OnSendError(
+ const std::string& app_id,
+ const GCMClient::SendErrorDetails& send_error_details) = 0;
+
+ // Called when a GCM message was received by GCM server.
+ virtual void OnSendAcknowledged(const std::string& app_id,
+ const std::string& message_id) = 0;
+
+ // Called when a GCM message has been received but decryption failed.
+ // |message_id| is a message identifier sent by the GCM server.
+ // |error_message| is human-readable description of the error, for reporting
+ // purposes. By default this handler does nothing.
+ virtual void OnMessageDecryptionFailed(const std::string& app_id,
+ const std::string& message_id,
+ const std::string& error_message);
+
+ // If no app handler has been added with the exact app_id of an incoming
+ // event, all handlers will be asked (in arbitrary order) whether they can
+ // handle the app_id, and the first to return true will receive the event.
+ virtual bool CanHandle(const std::string& app_id) const;
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_APP_HANDLER_H_
diff --git a/chromium/components/gcm_driver/gcm_backoff_policy.cc b/chromium/components/gcm_driver/gcm_backoff_policy.cc
new file mode 100644
index 00000000000..45ccd8b5b00
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_backoff_policy.cc
@@ -0,0 +1,46 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/gcm_backoff_policy.h"
+
+namespace gcm {
+
+namespace {
+
+// Backoff policy. Shared across GCM requests.
+// Note: In order to ensure a minimum of 20 seconds between server errors (for
+// server reasons), we have a 30s +- 10s (33%) jitter initial backoff.
+const net::BackoffEntry::Policy kDefaultBackoffPolicy = {
+ // Number of initial errors (in sequence) to ignore before applying
+ // exponential back-off rules.
+ 0,
+
+ // Initial delay for exponential back-off in ms.
+ 30 * 1000, // 30 seconds.
+
+ // Factor by which the waiting time will be multiplied.
+ 2,
+
+ // Fuzzing percentage. ex: 10% will spread requests randomly
+ // between 90%-100% of the calculated time.
+ 0.33, // 33%.
+
+ // Maximum amount of time we are willing to delay our request in ms.
+ 10 * 60 * 1000, // 10 minutes.
+
+ // Time to keep an entry from being discarded even when it
+ // has no significant state, -1 to never discard.
+ -1,
+
+ // Don't use initial delay unless the last request was an error.
+ false,
+};
+
+} // namespace
+
+const net::BackoffEntry::Policy& GetGCMBackoffPolicy() {
+ return kDefaultBackoffPolicy;
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_backoff_policy.h b/chromium/components/gcm_driver/gcm_backoff_policy.h
new file mode 100644
index 00000000000..d49accbadf7
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_backoff_policy.h
@@ -0,0 +1,17 @@
+// Copyright 2014 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 COMPONENTS_GCM_DRIVER_GCM_BACKOFF_POLICY_H_
+#define COMPONENTS_GCM_DRIVER_GCM_BACKOFF_POLICY_H_
+
+#include "net/base/backoff_entry.h"
+
+namespace gcm {
+
+// Returns the backoff policy that applies to all GCM requests.
+const net::BackoffEntry::Policy& GetGCMBackoffPolicy();
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_BACKOFF_POLICY_H_
diff --git a/chromium/components/gcm_driver/gcm_client.cc b/chromium/components/gcm_driver/gcm_client.cc
new file mode 100644
index 00000000000..cc9ccc1f764
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_client.cc
@@ -0,0 +1,38 @@
+// Copyright 2013 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.
+
+#include "components/gcm_driver/gcm_client.h"
+
+namespace gcm {
+
+GCMClient::ChromeBuildInfo::ChromeBuildInfo()
+ : platform(PLATFORM_UNSPECIFIED), channel(CHANNEL_UNKNOWN) {}
+
+GCMClient::ChromeBuildInfo::~ChromeBuildInfo() = default;
+
+GCMClient::SendErrorDetails::SendErrorDetails() : result(UNKNOWN_ERROR) {}
+
+GCMClient::SendErrorDetails::SendErrorDetails(const SendErrorDetails& other) =
+ default;
+
+GCMClient::SendErrorDetails::~SendErrorDetails() = default;
+
+GCMClient::GCMStatistics::GCMStatistics()
+ : is_recording(false),
+ gcm_client_created(false),
+ connection_client_created(false),
+ android_id(0u),
+ android_secret(0u),
+ send_queue_size(0),
+ resend_queue_size(0) {}
+
+GCMClient::GCMStatistics::GCMStatistics(const GCMStatistics& other) = default;
+
+GCMClient::GCMStatistics::~GCMStatistics() = default;
+
+GCMClient::GCMClient() = default;
+
+GCMClient::~GCMClient() = default;
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_client.h b/chromium/components/gcm_driver/gcm_client.h
new file mode 100644
index 00000000000..4ee187af8fd
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_client.h
@@ -0,0 +1,366 @@
+// Copyright 2014 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 COMPONENTS_GCM_DRIVER_GCM_CLIENT_H_
+#define COMPONENTS_GCM_DRIVER_GCM_CLIENT_H_
+
+#include <stdint.h>
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/memory/scoped_refptr.h"
+#include "components/gcm_driver/common/gcm_message.h"
+#include "components/gcm_driver/gcm_activity.h"
+#include "components/gcm_driver/registration_info.h"
+#include "google_apis/gaia/core_account_id.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "services/network/public/mojom/proxy_resolving_socket.mojom-forward.h"
+
+namespace base {
+class FilePath;
+class RetainingOneShotTimer;
+class SequencedTaskRunner;
+} // namespace base
+
+namespace net {
+class IPEndPoint;
+} // namespace net
+
+namespace network {
+class NetworkConnectionTracker;
+class SharedURLLoaderFactory;
+} // namespace network
+
+namespace gcm {
+
+struct AccountMapping;
+class Encryptor;
+enum class GCMDecryptionResult;
+
+// Interface that encapsulates the network communications with the Google Cloud
+// Messaging server. This interface is not supposed to be thread-safe.
+class GCMClient {
+ public:
+ // Controls how GCM is being started. At first, GCMClient will be initialized
+ // and GCM store will be loaded. Then GCM connection may or may not be
+ // initiated depending on this enum value.
+ enum StartMode {
+ // GCM should be started only when it is being actually used. If no
+ // registration record is found, GCM will not kick off.
+ DELAYED_START,
+ // GCM should be started immediately.
+ IMMEDIATE_START
+ };
+
+ // Used for UMA. Can add enum values, but never renumber or delete and reuse.
+ enum Result {
+ // Successful operation.
+ SUCCESS,
+ // Invalid parameter.
+ INVALID_PARAMETER,
+ // GCM is disabled.
+ GCM_DISABLED,
+ // Previous asynchronous operation is still pending to finish. Certain
+ // operation, like register, is only allowed one at a time.
+ ASYNC_OPERATION_PENDING,
+ // Network socket error.
+ NETWORK_ERROR,
+ // Problem at the server.
+ SERVER_ERROR,
+ // Exceeded the specified TTL during message sending.
+ TTL_EXCEEDED,
+ // Other errors.
+ UNKNOWN_ERROR,
+
+ // Used for UMA. Keep kMaxValue up to date and sync with histograms.xml.
+ kMaxValue = UNKNOWN_ERROR
+ };
+
+ enum ChromePlatform {
+ PLATFORM_WIN,
+ PLATFORM_MAC,
+ PLATFORM_LINUX,
+ PLATFORM_CROS,
+ PLATFORM_IOS,
+ PLATFORM_ANDROID,
+ PLATFORM_UNSPECIFIED
+ };
+
+ enum ChromeChannel {
+ CHANNEL_STABLE,
+ CHANNEL_BETA,
+ CHANNEL_DEV,
+ CHANNEL_CANARY,
+ CHANNEL_UNKNOWN
+ };
+
+ struct ChromeBuildInfo {
+ ChromeBuildInfo();
+ ~ChromeBuildInfo();
+
+ ChromePlatform platform;
+ ChromeChannel channel;
+ std::string version;
+ std::string product_category_for_subtypes;
+ };
+
+ // Detailed information of the Send Error event.
+ struct SendErrorDetails {
+ SendErrorDetails();
+ SendErrorDetails(const SendErrorDetails& other);
+ ~SendErrorDetails();
+
+ std::string message_id;
+ MessageData additional_data;
+ Result result;
+ };
+
+ // Internal states and activity statistics of a GCM client.
+ struct GCMStatistics {
+ public:
+ GCMStatistics();
+ GCMStatistics(const GCMStatistics& other);
+ ~GCMStatistics();
+
+ bool is_recording;
+ bool gcm_client_created;
+ std::string gcm_client_state;
+ bool connection_client_created;
+ std::string connection_state;
+ base::Time last_checkin;
+ base::Time next_checkin;
+ uint64_t android_id;
+ uint64_t android_secret;
+ std::vector<std::string> registered_app_ids;
+ int send_queue_size;
+ int resend_queue_size;
+
+ RecordedActivities recorded_activities;
+ };
+
+ // Information about account.
+ struct AccountTokenInfo {
+ CoreAccountId account_id;
+ std::string email;
+ std::string access_token;
+ };
+
+ // A delegate interface that allows the GCMClient instance to interact with
+ // its caller, i.e. notifying asynchronous event.
+ class Delegate {
+ public:
+ // Called when the registration completed successfully or an error occurs.
+ // |registration_info|: the specific information required for the
+ // registration.
+ // |registration_id|: non-empty if the registration completed successfully.
+ // |result|: the type of the error if an error occured, success otherwise.
+ virtual void OnRegisterFinished(
+ scoped_refptr<RegistrationInfo> registration_info,
+ const std::string& registration_id,
+ Result result) = 0;
+
+ // Called when the unregistration completed.
+ // |registration_info|: the specific information required for the
+ // registration.
+ // |result|: result of the unregistration.
+ virtual void OnUnregisterFinished(
+ scoped_refptr<RegistrationInfo> registration_info,
+ GCMClient::Result result) = 0;
+
+ // Called when the message is scheduled to send successfully or an error
+ // occurs.
+ // |app_id|: application ID.
+ // |message_id|: ID of the message being sent.
+ // |result|: the type of the error if an error occured, success otherwise.
+ virtual void OnSendFinished(const std::string& app_id,
+ const std::string& message_id,
+ Result result) = 0;
+
+ // Called when a message has been received.
+ // |app_id|: application ID.
+ // |message|: message received.
+ virtual void OnMessageReceived(const std::string& app_id,
+ const IncomingMessage& message) = 0;
+
+ // Called when some messages have been deleted from the server.
+ // |app_id|: application ID.
+ virtual void OnMessagesDeleted(const std::string& app_id) = 0;
+
+ // Called when a message failed to send to the server.
+ // |app_id|: application ID.
+ // |send_error_detials|: Details of the send error event, like mesasge ID.
+ virtual void OnMessageSendError(
+ const std::string& app_id,
+ const SendErrorDetails& send_error_details) = 0;
+
+ // Called when a message was acknowledged by the GCM server.
+ // |app_id|: application ID.
+ // |message_id|: ID of the acknowledged message.
+ virtual void OnSendAcknowledged(const std::string& app_id,
+ const std::string& message_id) = 0;
+
+ // Called when the GCM becomes ready. To get to this state, GCMClient
+ // finished loading from the GCM store and retrieved the device check-in
+ // from the server if it hadn't yet.
+ // |account_mappings|: a persisted list of accounts mapped to this GCM
+ // client.
+ // |last_token_fetch_time|: time of a last successful token fetch.
+ virtual void OnGCMReady(const std::vector<AccountMapping>& account_mappings,
+ const base::Time& last_token_fetch_time) = 0;
+
+ // Called when activities are being recorded and a new activity has just
+ // been recorded.
+ virtual void OnActivityRecorded() = 0;
+
+ // Called when a new connection is established and a successful handshake
+ // has been performed.
+ virtual void OnConnected(const net::IPEndPoint& ip_endpoint) = 0;
+
+ // Called when the connection is interrupted.
+ virtual void OnDisconnected() = 0;
+
+ // Called when the GCM store is reset (e.g. due to corruption), which
+ // changes the device ID, invalidating all prior registrations.
+ virtual void OnStoreReset() = 0;
+ };
+
+ GCMClient();
+ virtual ~GCMClient();
+
+ // Begins initialization of the GCM Client. This will not trigger a
+ // connection. Must be called on |io_task_runner|.
+ // |chrome_build_info|: chrome info, i.e., version, channel and etc.
+ // |store_path|: path to the GCM store.
+ // |remove_account_mappings_with_email_key|: Whether account mappings having
+ // email as account key should be removed while loading. Required
+ // during the migration of account identifier from email to Gaia ID.
+ // |blocking_task_runner|: for running blocking file tasks.
+ // |io_task_runner|: for running IO tasks. When provided, it could be a
+ // wrapper on top of base::ThreadTaskRunnerHandle::Get() to provide power
+ // management featueres so that a delayed task posted to it can wake the
+ // system up from sleep to perform the task.
+ // |get_socket_factory_callback|: a callback that can accept a receiver for a
+ // network::mojom::ProxyResolvingSocketFactory. It needs to be safe to
+ // run on any thread.
+ // |delegate|: the delegate whose methods will be called asynchronously in
+ // response to events and messages.
+ virtual void Initialize(
+ const ChromeBuildInfo& chrome_build_info,
+ const base::FilePath& store_path,
+ bool remove_account_mappings_with_email_key,
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner,
+ base::RepeatingCallback<void(
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)>
+ get_socket_factory_callback,
+ const scoped_refptr<network::SharedURLLoaderFactory>& url_loader_factory,
+ network::NetworkConnectionTracker* network_connection_tracker_,
+ std::unique_ptr<Encryptor> encryptor,
+ Delegate* delegate) = 0;
+
+ // This will initiate the GCM connection only if |start_mode| means to start
+ // the GCM immediately or the GCM registration records are found in the store.
+ // Note that it is OK to call Start multiple times and the implementation
+ // should handle it gracefully.
+ virtual void Start(StartMode start_mode) = 0;
+
+ // Stops using the GCM service. This will not erase the persisted data.
+ virtual void Stop() = 0;
+
+ // Registers with the server to access the provided service.
+ // Delegate::OnRegisterFinished will be called asynchronously upon completion.
+ // |registration_info|: the specific information required for the
+ // registration. For GCM, it will contain app id and
+ // sender IDs. For InstanceID, it will contain app_id,
+ // authorized entity and scope.
+ virtual void Register(scoped_refptr<RegistrationInfo> registration_info) = 0;
+
+ // Checks that the provided |registration_id| (aka token for Instance ID
+ // registrations) matches the stored registration info. Also checks sender IDs
+ // match for GCM registrations.
+ virtual bool ValidateRegistration(
+ scoped_refptr<RegistrationInfo> registration_info,
+ const std::string& registration_id) = 0;
+
+ // Unregisters from the server to stop accessing the provided service.
+ // Delegate::OnUnregisterFinished will be called asynchronously upon
+ // completion.
+ // |registration_info|: the specific information required for the
+ // registration. For GCM, it will contain app id (sender
+ // IDs can be ingored). For InstanceID, it will contain
+ // app id, authorized entity and scope.
+ virtual void Unregister(
+ scoped_refptr<RegistrationInfo> registration_info) = 0;
+
+ // Sends a message to a given receiver. Delegate::OnSendFinished will be
+ // called asynchronously upon completion.
+ // |app_id|: application ID.
+ // |receiver_id|: registration ID of the receiver party.
+ // |message|: message to be sent.
+ virtual void Send(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) = 0;
+
+ // Records a decryption failure due to |result| for the |app_id|.
+ virtual void RecordDecryptionFailure(const std::string& app_id,
+ GCMDecryptionResult result) = 0;
+
+ // Enables or disables internal activity recording.
+ virtual void SetRecording(bool recording) = 0;
+
+ // Clear all recorded GCM activity logs.
+ virtual void ClearActivityLogs() = 0;
+
+ // Gets internal states and statistics.
+ virtual GCMStatistics GetStatistics() const = 0;
+
+ // Sets a list of accounts with OAuth2 tokens for the next checkin.
+ // |account_tokens|: list of email addresses, account IDs and OAuth2 access
+ // tokens.
+ virtual void SetAccountTokens(
+ const std::vector<AccountTokenInfo>& account_tokens) = 0;
+
+ // Persists the |account_mapping| in the store.
+ virtual void UpdateAccountMapping(const AccountMapping& account_mapping) = 0;
+
+ // Removes the account mapping related to |account_id| from the persistent
+ // store.
+ virtual void RemoveAccountMapping(const CoreAccountId& account_id) = 0;
+
+ // Sets last token fetch time in persistent store.
+ virtual void SetLastTokenFetchTime(const base::Time& time) = 0;
+
+ // Updates the timer used by the HeartbeatManager for sending heartbeats.
+ virtual void UpdateHeartbeatTimer(
+ std::unique_ptr<base::RetainingOneShotTimer> timer) = 0;
+
+ // Adds the Instance ID data for a specific app to the persistent store.
+ virtual void AddInstanceIDData(const std::string& app_id,
+ const std::string& instance_id,
+ const std::string& extra_data) = 0;
+
+ // Removes the Instance ID data for a specific app from the persistent store.
+ virtual void RemoveInstanceIDData(const std::string& app_id) = 0;
+
+ // Retrieves the Instance ID data for a specific app from the persistent
+ // store.
+ virtual void GetInstanceIDData(const std::string& app_id,
+ std::string* instance_id,
+ std::string* extra_data) = 0;
+
+ // Gets and sets custom heartbeat interval for the MCS connection.
+ // |scope| is used to identify the component that requests a custom interval
+ // to be set, and allows that component to later revoke the setting. It should
+ // be unique.
+ virtual void AddHeartbeatInterval(const std::string& scope,
+ int interval_ms) = 0;
+ virtual void RemoveHeartbeatInterval(const std::string& scope) = 0;
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_CLIENT_H_
diff --git a/chromium/components/gcm_driver/gcm_client_factory.cc b/chromium/components/gcm_driver/gcm_client_factory.cc
new file mode 100644
index 00000000000..9ef8ad4e054
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_client_factory.cc
@@ -0,0 +1,23 @@
+// Copyright (c) 2014 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.
+
+#include "components/gcm_driver/gcm_client_factory.h"
+
+#include "base/memory/ptr_util.h"
+#include "components/gcm_driver/gcm_client_impl.h"
+
+namespace gcm {
+
+std::unique_ptr<GCMClient> GCMClientFactory::BuildInstance() {
+ return std::unique_ptr<GCMClient>(new GCMClientImpl(
+ base::WrapUnique<GCMInternalsBuilder>(new GCMInternalsBuilder())));
+}
+
+GCMClientFactory::GCMClientFactory() {
+}
+
+GCMClientFactory::~GCMClientFactory() {
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_client_factory.h b/chromium/components/gcm_driver/gcm_client_factory.h
new file mode 100644
index 00000000000..a2121cdce62
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_client_factory.h
@@ -0,0 +1,30 @@
+// Copyright (c) 2014 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 COMPONENTS_GCM_DRIVER_GCM_CLIENT_FACTORY_H_
+#define COMPONENTS_GCM_DRIVER_GCM_CLIENT_FACTORY_H_
+
+#include <memory>
+
+namespace gcm {
+
+class GCMClient;
+
+class GCMClientFactory {
+ public:
+ GCMClientFactory();
+
+ GCMClientFactory(const GCMClientFactory&) = delete;
+ GCMClientFactory& operator=(const GCMClientFactory&) = delete;
+
+ virtual ~GCMClientFactory();
+
+ // Creates a new instance of GCMClient. The testing code could override this
+ // to provide a mocked instance.
+ virtual std::unique_ptr<GCMClient> BuildInstance();
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_CLIENT_FACTORY_H_
diff --git a/chromium/components/gcm_driver/gcm_client_impl.cc b/chromium/components/gcm_driver/gcm_client_impl.cc
new file mode 100644
index 00000000000..016228a0326
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_client_impl.cc
@@ -0,0 +1,1481 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/gcm_client_impl.h"
+
+#include <stddef.h>
+
+#include <memory>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/files/file_path.h"
+#include "base/location.h"
+#include "base/logging.h"
+#include "base/memory/ptr_util.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/task/single_thread_task_runner.h"
+#include "base/time/default_clock.h"
+#include "base/timer/timer.h"
+#include "components/crx_file/id_util.h"
+#include "components/gcm_driver/crypto/gcm_decryption_result.h"
+#include "components/gcm_driver/features.h"
+#include "components/gcm_driver/gcm_account_mapper.h"
+#include "components/gcm_driver/gcm_backoff_policy.h"
+#include "google_apis/gcm/base/encryptor.h"
+#include "google_apis/gcm/base/mcs_message.h"
+#include "google_apis/gcm/base/mcs_util.h"
+#include "google_apis/gcm/engine/checkin_request.h"
+#include "google_apis/gcm/engine/connection_factory_impl.h"
+#include "google_apis/gcm/engine/gcm_registration_request_handler.h"
+#include "google_apis/gcm/engine/gcm_store_impl.h"
+#include "google_apis/gcm/engine/gcm_unregistration_request_handler.h"
+#include "google_apis/gcm/engine/instance_id_delete_token_request_handler.h"
+#include "google_apis/gcm/engine/instance_id_get_token_request_handler.h"
+#include "google_apis/gcm/monitoring/gcm_stats_recorder.h"
+#include "google_apis/gcm/protocol/checkin.pb.h"
+#include "google_apis/gcm/protocol/mcs.pb.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+#include "url/gurl.h"
+
+namespace gcm {
+
+// It is okay to append to the enum if these states grow. DO NOT reorder,
+// renumber or otherwise reuse existing values.
+// Do not assign an explicit value to REGISTRATION_CACHE_STATUS_COUNT, as
+// this lets the compiler keep it up to date.
+enum class RegistrationCacheStatus {
+ REGISTRATION_NOT_FOUND = 0,
+ REGISTRATION_FOUND_AND_FRESH = 1,
+ REGISTRATION_FOUND_BUT_STALE = 2,
+ REGISTRATION_FOUND_BUT_SENDERS_DONT_MATCH = 3,
+ // NOTE: always keep this entry at the end. Add new value only immediately
+ // above this line. Make sure to update the corresponding histogram enum
+ // accordingly.
+ REGISTRATION_CACHE_STATUS_COUNT
+};
+
+namespace {
+
+// Indicates a message type of the received message.
+enum MessageType {
+ UNKNOWN, // Undetermined type.
+ DATA_MESSAGE, // Regular data message.
+ DELETED_MESSAGES, // Messages were deleted on the server.
+ SEND_ERROR, // Error sending a message.
+};
+
+enum ResetStoreError {
+ DESTROYING_STORE_FAILED,
+ INFINITE_STORE_RESET,
+ // NOTE: always keep this entry at the end. Add new value only immediately
+ // above this line. Make sure to update the corresponding histogram enum
+ // accordingly.
+ RESET_STORE_ERROR_COUNT
+};
+
+const int kMaxRegistrationRetries = 5;
+const int kMaxUnregistrationRetries = 5;
+const char kDeletedCountKey[] = "total_deleted";
+const char kMessageTypeDataMessage[] = "gcm";
+const char kMessageTypeDeletedMessagesKey[] = "deleted_messages";
+const char kMessageTypeKey[] = "message_type";
+const char kMessageTypeSendErrorKey[] = "send_error";
+const char kSubtypeKey[] = "subtype";
+const char kSendMessageFromValue[] = "gcm@chrome.com";
+const int64_t kDefaultUserSerialNumber = 0LL;
+const int kDestroyGCMStoreDelayMS = 5 * 60 * 1000; // 5 minutes.
+
+GCMClient::Result ToGCMClientResult(MCSClient::MessageSendStatus status) {
+ switch (status) {
+ case MCSClient::QUEUED:
+ return GCMClient::SUCCESS;
+ case MCSClient::MESSAGE_TOO_LARGE:
+ return GCMClient::INVALID_PARAMETER;
+ case MCSClient::QUEUE_SIZE_LIMIT_REACHED:
+ case MCSClient::APP_QUEUE_SIZE_LIMIT_REACHED:
+ case MCSClient::NO_CONNECTION_ON_ZERO_TTL:
+ case MCSClient::TTL_EXCEEDED:
+ return GCMClient::NETWORK_ERROR;
+ case MCSClient::SENT:
+ case MCSClient::SEND_STATUS_COUNT:
+ NOTREACHED();
+ break;
+ }
+ return GCMClientImpl::UNKNOWN_ERROR;
+}
+
+void ToCheckinProtoVersion(
+ const GCMClient::ChromeBuildInfo& chrome_build_info,
+ checkin_proto::ChromeBuildProto* android_build_info) {
+ checkin_proto::ChromeBuildProto_Platform platform =
+ checkin_proto::ChromeBuildProto_Platform_PLATFORM_LINUX;
+ switch (chrome_build_info.platform) {
+ case GCMClient::PLATFORM_WIN:
+ platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_WIN;
+ break;
+ case GCMClient::PLATFORM_MAC:
+ platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_MAC;
+ break;
+ case GCMClient::PLATFORM_LINUX:
+ platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_LINUX;
+ break;
+ case GCMClient::PLATFORM_IOS:
+ platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_IOS;
+ break;
+ case GCMClient::PLATFORM_ANDROID:
+ platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_ANDROID;
+ break;
+ case GCMClient::PLATFORM_CROS:
+ platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_CROS;
+ break;
+ case GCMClient::PLATFORM_UNSPECIFIED:
+ // For unknown platform, return as LINUX.
+ platform = checkin_proto::ChromeBuildProto_Platform_PLATFORM_LINUX;
+ break;
+ }
+ android_build_info->set_platform(platform);
+
+ checkin_proto::ChromeBuildProto_Channel channel =
+ checkin_proto::ChromeBuildProto_Channel_CHANNEL_UNKNOWN;
+ switch (chrome_build_info.channel) {
+ case GCMClient::CHANNEL_STABLE:
+ channel = checkin_proto::ChromeBuildProto_Channel_CHANNEL_STABLE;
+ break;
+ case GCMClient::CHANNEL_BETA:
+ channel = checkin_proto::ChromeBuildProto_Channel_CHANNEL_BETA;
+ break;
+ case GCMClient::CHANNEL_DEV:
+ channel = checkin_proto::ChromeBuildProto_Channel_CHANNEL_DEV;
+ break;
+ case GCMClient::CHANNEL_CANARY:
+ channel = checkin_proto::ChromeBuildProto_Channel_CHANNEL_CANARY;
+ break;
+ case GCMClient::CHANNEL_UNKNOWN:
+ channel = checkin_proto::ChromeBuildProto_Channel_CHANNEL_UNKNOWN;
+ break;
+ }
+ android_build_info->set_channel(channel);
+
+ android_build_info->set_chrome_version(chrome_build_info.version);
+}
+
+MessageType DecodeMessageType(const std::string& value) {
+ if (kMessageTypeDeletedMessagesKey == value)
+ return DELETED_MESSAGES;
+ if (kMessageTypeSendErrorKey == value)
+ return SEND_ERROR;
+ if (kMessageTypeDataMessage == value)
+ return DATA_MESSAGE;
+ return UNKNOWN;
+}
+
+int ConstructGCMVersion(const std::string& chrome_version) {
+ // Major Chrome version is passed as GCM version.
+ size_t pos = chrome_version.find('.');
+ if (pos == std::string::npos) {
+ NOTREACHED();
+ return 0;
+ }
+
+ int gcm_version = 0;
+ base::StringToInt(
+ base::StringPiece(chrome_version.c_str(), pos), &gcm_version);
+ return gcm_version;
+}
+
+std::string SerializeInstanceIDData(const std::string& instance_id,
+ const std::string& extra_data) {
+ DCHECK(!instance_id.empty() && !extra_data.empty());
+ DCHECK(instance_id.find(',') == std::string::npos);
+ return instance_id + "," + extra_data;
+}
+
+bool DeserializeInstanceIDData(const std::string& serialized_data,
+ std::string* instance_id,
+ std::string* extra_data) {
+ DCHECK(instance_id && extra_data);
+ std::size_t pos = serialized_data.find(',');
+ if (pos == std::string::npos)
+ return false;
+ *instance_id = serialized_data.substr(0, pos);
+ *extra_data = serialized_data.substr(pos + 1);
+ return !instance_id->empty() && !extra_data->empty();
+}
+
+bool InstanceIDUsesSubtypeForAppId(const std::string& app_id) {
+ // Always use subtypes with Instance ID, except for Chrome Apps/Extensions.
+ return !crx_file::id_util::IdIsValid(app_id);
+}
+
+void RecordResetStoreErrorToUMA(ResetStoreError error) {
+ UMA_HISTOGRAM_ENUMERATION("GCM.ResetStore", error, RESET_STORE_ERROR_COUNT);
+}
+
+} // namespace
+
+void RecordRegistrationRequestToUMA(gcm::RegistrationCacheStatus status) {
+ UMA_HISTOGRAM_ENUMERATION(
+ "GCM.RegistrationCacheStatus", status,
+ RegistrationCacheStatus::REGISTRATION_CACHE_STATUS_COUNT);
+}
+GCMInternalsBuilder::GCMInternalsBuilder() {}
+GCMInternalsBuilder::~GCMInternalsBuilder() {}
+
+base::Clock* GCMInternalsBuilder::GetClock() {
+ return base::DefaultClock::GetInstance();
+}
+
+std::unique_ptr<MCSClient> GCMInternalsBuilder::BuildMCSClient(
+ const std::string& version,
+ base::Clock* clock,
+ ConnectionFactory* connection_factory,
+ GCMStore* gcm_store,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner,
+ GCMStatsRecorder* recorder) {
+ return std::make_unique<MCSClient>(version, clock, connection_factory,
+ gcm_store, std::move(io_task_runner),
+ recorder);
+}
+
+std::unique_ptr<ConnectionFactory> GCMInternalsBuilder::BuildConnectionFactory(
+ const std::vector<GURL>& endpoints,
+ const net::BackoffEntry::Policy& backoff_policy,
+ base::RepeatingCallback<void(
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)>
+ get_socket_factory_callback,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner,
+ GCMStatsRecorder* recorder,
+ network::NetworkConnectionTracker* network_connection_tracker) {
+ return std::make_unique<ConnectionFactoryImpl>(
+ endpoints, backoff_policy, std::move(get_socket_factory_callback),
+ std::move(io_task_runner), recorder, network_connection_tracker);
+}
+
+GCMClientImpl::CheckinInfo::CheckinInfo()
+ : android_id(0), secret(0), accounts_set(false) {
+}
+
+GCMClientImpl::CheckinInfo::~CheckinInfo() {
+}
+
+void GCMClientImpl::CheckinInfo::SnapshotCheckinAccounts() {
+ last_checkin_accounts.clear();
+ for (auto iter = account_tokens.begin(); iter != account_tokens.end();
+ ++iter) {
+ last_checkin_accounts.insert(iter->first);
+ }
+}
+
+void GCMClientImpl::CheckinInfo::Reset() {
+ android_id = 0;
+ secret = 0;
+ accounts_set = false;
+ account_tokens.clear();
+ last_checkin_accounts.clear();
+}
+
+GCMClientImpl::GCMClientImpl(
+ std::unique_ptr<GCMInternalsBuilder> internals_builder)
+ : internals_builder_(std::move(internals_builder)),
+ state_(UNINITIALIZED),
+ delegate_(nullptr),
+ start_mode_(DELAYED_START),
+ clock_(internals_builder_->GetClock()),
+ gcm_store_reset_(false),
+ network_connection_tracker_(nullptr) {}
+
+GCMClientImpl::~GCMClientImpl() {
+}
+
+void GCMClientImpl::Initialize(
+ const ChromeBuildInfo& chrome_build_info,
+ const base::FilePath& path,
+ bool remove_account_mappings_with_email_key,
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner,
+ base::RepeatingCallback<void(
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)>
+ get_socket_factory_callback,
+ const scoped_refptr<network::SharedURLLoaderFactory>& url_loader_factory,
+ network::NetworkConnectionTracker* network_connection_tracker,
+ std::unique_ptr<Encryptor> encryptor,
+ GCMClient::Delegate* delegate) {
+ DCHECK_EQ(UNINITIALIZED, state_);
+ DCHECK(delegate);
+ DCHECK(io_task_runner);
+ DCHECK(io_task_runner->RunsTasksInCurrentSequence());
+
+ get_socket_factory_callback_ = std::move(get_socket_factory_callback);
+ url_loader_factory_ = url_loader_factory;
+ network_connection_tracker_ = network_connection_tracker;
+ chrome_build_info_ = chrome_build_info;
+ gcm_store_ = std::make_unique<GCMStoreImpl>(
+ path, remove_account_mappings_with_email_key, blocking_task_runner,
+ std::move(encryptor));
+ delegate_ = delegate;
+ io_task_runner_ = std::move(io_task_runner);
+ recorder_.SetDelegate(this);
+ state_ = INITIALIZED;
+}
+
+void GCMClientImpl::Start(StartMode start_mode) {
+ DCHECK_NE(UNINITIALIZED, state_);
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+
+ if (state_ == LOADED) {
+ // Start the GCM if not yet.
+ if (start_mode == IMMEDIATE_START) {
+ // Give up the scheduling to wipe out the store since now some one starts
+ // to use GCM.
+ destroying_gcm_store_ptr_factory_.InvalidateWeakPtrs();
+
+ StartGCM();
+ }
+ return;
+ }
+
+ // The delay start behavior will be abandoned when Start has been called
+ // once with IMMEDIATE_START behavior.
+ if (start_mode == IMMEDIATE_START)
+ start_mode_ = IMMEDIATE_START;
+
+ // Bail out if the loading is not started or completed.
+ if (state_ != INITIALIZED)
+ return;
+
+ // Once the loading is completed, the check-in will be initiated.
+ // If we're in lazy start mode, don't create a new store since none is really
+ // using GCM functionality yet.
+ gcm_store_->Load((start_mode == IMMEDIATE_START) ? GCMStore::CREATE_IF_MISSING
+ : GCMStore::DO_NOT_CREATE,
+ base::BindOnce(&GCMClientImpl::OnLoadCompleted,
+ weak_ptr_factory_.GetWeakPtr()));
+ state_ = LOADING;
+}
+
+void GCMClientImpl::OnLoadCompleted(
+ std::unique_ptr<GCMStore::LoadResult> result) {
+ DCHECK_EQ(LOADING, state_);
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+
+ if (!result->success) {
+ if (result->store_does_not_exist) {
+ if (start_mode_ == IMMEDIATE_START) {
+ // An immediate start was requested during the delayed start that just
+ // completed. Perform it now.
+ gcm_store_->Load(GCMStore::CREATE_IF_MISSING,
+ base::BindOnce(&GCMClientImpl::OnLoadCompleted,
+ weak_ptr_factory_.GetWeakPtr()));
+ } else {
+ // In the case that the store does not exist, set |state_| back to
+ // INITIALIZED such that store loading could be triggered again when
+ // Start() is called with IMMEDIATE_START.
+ state_ = INITIALIZED;
+ }
+ } else {
+ // Otherwise, destroy the store to try again.
+ ResetStore();
+ }
+ return;
+ }
+ gcm_store_reset_ = false;
+
+ device_checkin_info_.android_id = result->device_android_id;
+ device_checkin_info_.secret = result->device_security_token;
+ device_checkin_info_.last_checkin_accounts = result->last_checkin_accounts;
+ // A case where there were previously no accounts reported with checkin is
+ // considered to be the same as when the list of accounts is empty. It enables
+ // scheduling a periodic checkin for devices with no signed in users
+ // immediately after restart, while keeping |accounts_set == false| delays the
+ // checkin until the list of accounts is set explicitly.
+ if (result->last_checkin_accounts.size() == 0)
+ device_checkin_info_.accounts_set = true;
+ last_checkin_time_ = result->last_checkin_time;
+ gservices_settings_.UpdateFromLoadResult(*result);
+
+ for (auto iter = result->registrations.begin();
+ iter != result->registrations.end();
+ ++iter) {
+ std::string registration_id;
+ scoped_refptr<RegistrationInfo> registration =
+ RegistrationInfo::BuildFromString(iter->first, iter->second,
+ &registration_id);
+ // TODO(jianli): Add UMA to track the error case.
+ if (registration)
+ registrations_.emplace(std::move(registration), registration_id);
+ }
+
+ for (auto iter = result->instance_id_data.begin();
+ iter != result->instance_id_data.end();
+ ++iter) {
+ std::string instance_id;
+ std::string extra_data;
+ if (DeserializeInstanceIDData(iter->second, &instance_id, &extra_data))
+ instance_id_data_[iter->first] = std::make_pair(instance_id, extra_data);
+ }
+
+ load_result_ = std::move(result);
+ state_ = LOADED;
+
+ // Don't initiate the GCM connection when GCM is in delayed start mode and
+ // not any standalone app has registered GCM yet.
+ if (start_mode_ == DELAYED_START && !HasStandaloneRegisteredApp()) {
+ // If no standalone app is using GCM and the device ID is present, schedule
+ // to have the store wiped out.
+ if (device_checkin_info_.android_id) {
+ io_task_runner_->PostDelayedTask(
+ FROM_HERE,
+ base::BindOnce(&GCMClientImpl::DestroyStoreWhenNotNeeded,
+ destroying_gcm_store_ptr_factory_.GetWeakPtr()),
+ base::Milliseconds(kDestroyGCMStoreDelayMS));
+ }
+
+ return;
+ }
+
+ StartGCM();
+}
+
+void GCMClientImpl::StartGCM() {
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+
+ // Taking over the value of account_mappings before passing the ownership of
+ // load result to InitializeMCSClient.
+ std::vector<AccountMapping> account_mappings;
+ account_mappings.swap(load_result_->account_mappings);
+ base::Time last_token_fetch_time = load_result_->last_token_fetch_time;
+
+ InitializeMCSClient();
+
+ if (device_checkin_info_.IsValid()) {
+ SchedulePeriodicCheckin();
+ OnReady(account_mappings, last_token_fetch_time);
+ return;
+ }
+
+ state_ = INITIAL_DEVICE_CHECKIN;
+ device_checkin_info_.Reset();
+ StartCheckin();
+}
+
+void GCMClientImpl::InitializeMCSClient() {
+ DCHECK(network_connection_tracker_);
+ std::vector<GURL> endpoints;
+ endpoints.push_back(gservices_settings_.GetMCSMainEndpoint());
+ GURL fallback_endpoint = gservices_settings_.GetMCSFallbackEndpoint();
+ if (fallback_endpoint.is_valid())
+ endpoints.push_back(fallback_endpoint);
+ connection_factory_ = internals_builder_->BuildConnectionFactory(
+ endpoints, GetGCMBackoffPolicy(), get_socket_factory_callback_,
+ io_task_runner_, &recorder_, network_connection_tracker_);
+ connection_factory_->SetConnectionListener(this);
+ mcs_client_ = internals_builder_->BuildMCSClient(
+ chrome_build_info_.version, clock_, connection_factory_.get(),
+ gcm_store_.get(), io_task_runner_, &recorder_);
+
+ mcs_client_->Initialize(
+ base::BindRepeating(&GCMClientImpl::OnMCSError,
+ weak_ptr_factory_.GetWeakPtr()),
+ base::BindRepeating(&GCMClientImpl::OnMessageReceivedFromMCS,
+ weak_ptr_factory_.GetWeakPtr()),
+ base::BindRepeating(&GCMClientImpl::OnMessageSentToMCS,
+ weak_ptr_factory_.GetWeakPtr()),
+ std::move(load_result_));
+}
+
+void GCMClientImpl::OnFirstTimeDeviceCheckinCompleted(
+ const CheckinInfo& checkin_info) {
+ DCHECK(!device_checkin_info_.IsValid());
+
+ device_checkin_info_.android_id = checkin_info.android_id;
+ device_checkin_info_.secret = checkin_info.secret;
+ // If accounts were not set by now, we can consider them set (to empty list)
+ // to make sure periodic checkins get scheduled after initial checkin.
+ device_checkin_info_.accounts_set = true;
+ gcm_store_->SetDeviceCredentials(
+ checkin_info.android_id, checkin_info.secret,
+ base::BindOnce(&GCMClientImpl::SetDeviceCredentialsCallback,
+ weak_ptr_factory_.GetWeakPtr()));
+
+ OnReady(std::vector<AccountMapping>(), base::Time());
+}
+
+void GCMClientImpl::OnReady(const std::vector<AccountMapping>& account_mappings,
+ const base::Time& last_token_fetch_time) {
+ state_ = READY;
+ StartMCSLogin();
+
+ delegate_->OnGCMReady(account_mappings, last_token_fetch_time);
+}
+
+void GCMClientImpl::StartMCSLogin() {
+ DCHECK_EQ(READY, state_);
+ DCHECK(device_checkin_info_.IsValid());
+ mcs_client_->Login(device_checkin_info_.android_id,
+ device_checkin_info_.secret);
+}
+
+void GCMClientImpl::DestroyStoreWhenNotNeeded() {
+ if (state_ != LOADED || start_mode_ != DELAYED_START)
+ return;
+
+ gcm_store_->Destroy(base::BindOnce(&GCMClientImpl::DestroyStoreCallback,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+void GCMClientImpl::ResetStore() {
+ // If already being reset, don't do it again. We want to prevent from
+ // resetting and loading from the store again and again.
+ if (gcm_store_reset_) {
+ RecordResetStoreErrorToUMA(INFINITE_STORE_RESET);
+ state_ = UNINITIALIZED;
+ return;
+ }
+ gcm_store_reset_ = true;
+
+ // Destroy the GCM store to start over.
+ gcm_store_->Destroy(base::BindOnce(&GCMClientImpl::ResetStoreCallback,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+void GCMClientImpl::SetAccountTokens(
+ const std::vector<AccountTokenInfo>& account_tokens) {
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+
+ device_checkin_info_.account_tokens.clear();
+ for (auto iter = account_tokens.begin(); iter != account_tokens.end();
+ ++iter) {
+ device_checkin_info_.account_tokens[iter->email] = iter->access_token;
+ }
+
+ bool accounts_set_before = device_checkin_info_.accounts_set;
+ device_checkin_info_.accounts_set = true;
+
+ DVLOG(1) << "Set account called with: " << account_tokens.size()
+ << " accounts.";
+
+ if (state_ != READY && state_ != INITIAL_DEVICE_CHECKIN)
+ return;
+
+ bool account_removed = false;
+ for (auto iter = device_checkin_info_.last_checkin_accounts.begin();
+ iter != device_checkin_info_.last_checkin_accounts.end(); ++iter) {
+ if (device_checkin_info_.account_tokens.find(*iter) ==
+ device_checkin_info_.account_tokens.end()) {
+ account_removed = true;
+ }
+ }
+
+ // Checkin will be forced when any of the accounts was removed during the
+ // current Chrome session or if there has been an account removed between the
+ // restarts of Chrome. If there is a checkin in progress, it will be canceled.
+ // We only force checkin when user signs out. When there is a new account
+ // signed in, the periodic checkin will take care of adding the association in
+ // reasonable time.
+ if (account_removed) {
+ DVLOG(1) << "Detected that account has been removed. Forcing checkin.";
+ checkin_request_.reset();
+ StartCheckin();
+ } else if (!accounts_set_before) {
+ SchedulePeriodicCheckin();
+ DVLOG(1) << "Accounts set for the first time. Scheduled periodic checkin.";
+ }
+}
+
+void GCMClientImpl::UpdateAccountMapping(
+ const AccountMapping& account_mapping) {
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+ gcm_store_->AddAccountMapping(
+ account_mapping, base::BindOnce(&GCMClientImpl::DefaultStoreCallback,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+void GCMClientImpl::RemoveAccountMapping(const CoreAccountId& account_id) {
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+ gcm_store_->RemoveAccountMapping(
+ account_id, base::BindOnce(&GCMClientImpl::DefaultStoreCallback,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+void GCMClientImpl::SetLastTokenFetchTime(const base::Time& time) {
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+ gcm_store_->SetLastTokenFetchTime(
+ time,
+ base::BindOnce(&GCMClientImpl::IgnoreWriteResultCallback,
+ weak_ptr_factory_.GetWeakPtr(),
+ /*operation_suffix_for_uma=*/"SetLastTokenFetchTime"));
+}
+
+void GCMClientImpl::UpdateHeartbeatTimer(
+ std::unique_ptr<base::RetainingOneShotTimer> timer) {
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+ DCHECK(mcs_client_);
+ mcs_client_->UpdateHeartbeatTimer(std::move(timer));
+}
+
+void GCMClientImpl::AddInstanceIDData(const std::string& app_id,
+ const std::string& instance_id,
+ const std::string& extra_data) {
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+ instance_id_data_[app_id] = std::make_pair(instance_id, extra_data);
+ // TODO(crbug/1028761): If this call fails, we likely leak a registration
+ // (the one stored in instance_id_data_ would be used for a registration but
+ // not persisted).
+ gcm_store_->AddInstanceIDData(
+ app_id, SerializeInstanceIDData(instance_id, extra_data),
+ base::BindOnce(&GCMClientImpl::IgnoreWriteResultCallback,
+ weak_ptr_factory_.GetWeakPtr(),
+ /*operation_suffix_for_uma=*/"AddInstanceIDData"));
+}
+
+void GCMClientImpl::RemoveInstanceIDData(const std::string& app_id) {
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+ instance_id_data_.erase(app_id);
+ gcm_store_->RemoveInstanceIDData(
+ app_id,
+ base::BindOnce(&GCMClientImpl::IgnoreWriteResultCallback,
+ weak_ptr_factory_.GetWeakPtr(),
+ /*operation_suffix_for_uma=*/"RemoveInstanceIDData"));
+}
+
+void GCMClientImpl::GetInstanceIDData(const std::string& app_id,
+ std::string* instance_id,
+ std::string* extra_data) {
+ DCHECK(instance_id && extra_data);
+
+ auto iter = instance_id_data_.find(app_id);
+ if (iter == instance_id_data_.end())
+ return;
+ *instance_id = iter->second.first;
+ *extra_data = iter->second.second;
+}
+
+void GCMClientImpl::AddHeartbeatInterval(const std::string& scope,
+ int interval_ms) {
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+ DCHECK(mcs_client_);
+ mcs_client_->AddHeartbeatInterval(scope, interval_ms);
+}
+
+void GCMClientImpl::RemoveHeartbeatInterval(const std::string& scope) {
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+ DCHECK(mcs_client_);
+ mcs_client_->RemoveHeartbeatInterval(scope);
+}
+
+void GCMClientImpl::StartCheckin() {
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+
+ // Make sure no checkin is in progress.
+ if (checkin_request_)
+ return;
+
+ checkin_proto::ChromeBuildProto chrome_build_proto;
+ ToCheckinProtoVersion(chrome_build_info_, &chrome_build_proto);
+ CheckinRequest::RequestInfo request_info(device_checkin_info_.android_id,
+ device_checkin_info_.secret,
+ device_checkin_info_.account_tokens,
+ gservices_settings_.digest(),
+ chrome_build_proto);
+ checkin_request_ = std::make_unique<CheckinRequest>(
+ gservices_settings_.GetCheckinURL(), request_info, GetGCMBackoffPolicy(),
+ base::BindOnce(&GCMClientImpl::OnCheckinCompleted,
+ weak_ptr_factory_.GetWeakPtr()),
+ url_loader_factory_, io_task_runner_, &recorder_);
+ // Taking a snapshot of the accounts count here, as there might be an asynch
+ // update of the account tokens while checkin is in progress.
+ device_checkin_info_.SnapshotCheckinAccounts();
+ checkin_request_->Start();
+}
+
+void GCMClientImpl::OnCheckinCompleted(
+ net::HttpStatusCode response_code,
+ const checkin_proto::AndroidCheckinResponse& checkin_response) {
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+
+ checkin_request_.reset();
+
+ if (response_code == net::HTTP_UNAUTHORIZED ||
+ response_code == net::HTTP_BAD_REQUEST) {
+ LOG(ERROR) << "Checkin rejected. Resetting GCM Store.";
+ ResetStore();
+ return;
+ }
+
+ DCHECK(checkin_response.has_android_id());
+ DCHECK(checkin_response.has_security_token());
+ CheckinInfo checkin_info;
+ checkin_info.android_id = checkin_response.android_id();
+ checkin_info.secret = checkin_response.security_token();
+
+ if (state_ == INITIAL_DEVICE_CHECKIN) {
+ OnFirstTimeDeviceCheckinCompleted(checkin_info);
+ } else {
+ // checkin_info is not expected to change after a periodic checkin as it
+ // would invalidate the registration IDs.
+ DCHECK_EQ(READY, state_);
+ DCHECK_EQ(device_checkin_info_.android_id, checkin_info.android_id);
+ DCHECK_EQ(device_checkin_info_.secret, checkin_info.secret);
+ }
+
+ if (device_checkin_info_.IsValid()) {
+ // First update G-services settings, as something might have changed.
+ if (gservices_settings_.UpdateFromCheckinResponse(checkin_response)) {
+ gcm_store_->SetGServicesSettings(
+ gservices_settings_.settings_map(), gservices_settings_.digest(),
+ base::BindOnce(&GCMClientImpl::SetGServicesSettingsCallback,
+ weak_ptr_factory_.GetWeakPtr()));
+ }
+
+ last_checkin_time_ = clock_->Now();
+ gcm_store_->SetLastCheckinInfo(
+ last_checkin_time_, device_checkin_info_.last_checkin_accounts,
+ base::BindOnce(&GCMClientImpl::SetLastCheckinInfoCallback,
+ weak_ptr_factory_.GetWeakPtr()));
+ SchedulePeriodicCheckin();
+ }
+}
+
+void GCMClientImpl::SetGServicesSettingsCallback(bool success) {
+ DCHECK(success);
+}
+
+void GCMClientImpl::SchedulePeriodicCheckin() {
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+
+ // Make sure no checkin is in progress.
+ if (checkin_request_.get() || !device_checkin_info_.accounts_set)
+ return;
+
+ // There should be only one periodic checkin pending at a time. Removing
+ // pending periodic checkin to schedule a new one.
+ periodic_checkin_ptr_factory_.InvalidateWeakPtrs();
+
+ base::TimeDelta time_to_next_checkin = GetTimeToNextCheckin();
+ if (time_to_next_checkin.is_negative())
+ time_to_next_checkin = base::TimeDelta();
+
+ io_task_runner_->PostDelayedTask(
+ FROM_HERE,
+ base::BindOnce(&GCMClientImpl::StartCheckin,
+ periodic_checkin_ptr_factory_.GetWeakPtr()),
+ time_to_next_checkin);
+}
+
+base::TimeDelta GCMClientImpl::GetTimeToNextCheckin() const {
+ return last_checkin_time_ + gservices_settings_.GetCheckinInterval() -
+ clock_->Now();
+}
+
+void GCMClientImpl::SetLastCheckinInfoCallback(bool success) {
+ // TODO(fgorski): This is one of the signals that store needs a rebuild.
+ DCHECK(success);
+}
+
+void GCMClientImpl::SetDeviceCredentialsCallback(bool success) {
+ // TODO(fgorski): This is one of the signals that store needs a rebuild.
+ DCHECK(success);
+}
+
+void GCMClientImpl::UpdateRegistrationCallback(bool success) {
+ // TODO(fgorski): This is one of the signals that store needs a rebuild.
+ DCHECK(success);
+}
+
+void GCMClientImpl::DefaultStoreCallback(bool success) {
+ DCHECK(success);
+}
+
+void GCMClientImpl::IgnoreWriteResultCallback(
+ const std::string& operation_suffix_for_uma,
+ bool success) {
+ // TODO(crbug.com/1081149): Implement proper error handling.
+ // TODO(fgorski): Ignoring the write result for now to make sure
+ // sync_intergration_tests are not broken.
+}
+
+void GCMClientImpl::DestroyStoreCallback(bool success) {
+ ResetCache();
+
+ if (!success) {
+ LOG(ERROR) << "Failed to destroy GCM store";
+ RecordResetStoreErrorToUMA(DESTROYING_STORE_FAILED);
+ state_ = UNINITIALIZED;
+ return;
+ }
+
+ state_ = INITIALIZED;
+}
+
+void GCMClientImpl::ResetStoreCallback(bool success) {
+ // Even an incomplete reset may invalidate registrations, and this might be
+ // the only opportunity to notify the delegate. For example a partial reset
+ // that deletes the "CURRENT" file will cause GCMStoreImpl to consider the DB
+ // to no longer exist, in which case the next load will simply create a new
+ // store rather than resetting it.
+ delegate_->OnStoreReset();
+
+ if (!success) {
+ LOG(ERROR) << "Failed to reset GCM store";
+ RecordResetStoreErrorToUMA(DESTROYING_STORE_FAILED);
+ state_ = UNINITIALIZED;
+ return;
+ }
+
+ state_ = INITIALIZED;
+ Start(start_mode_);
+}
+
+void GCMClientImpl::Stop() {
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+ // TODO(fgorski): Perhaps we should make a distinction between a Stop and a
+ // Shutdown.
+ DVLOG(1) << "Stopping the GCM Client";
+ ResetCache();
+ state_ = INITIALIZED;
+ gcm_store_->Close();
+}
+
+void GCMClientImpl::ResetCache() {
+ weak_ptr_factory_.InvalidateWeakPtrs();
+ periodic_checkin_ptr_factory_.InvalidateWeakPtrs();
+ device_checkin_info_.Reset();
+ connection_factory_.reset();
+ delegate_->OnDisconnected();
+ mcs_client_.reset();
+ checkin_request_.reset();
+ // Delete all of the pending registration and unregistration requests.
+ pending_registration_requests_.clear();
+ pending_unregistration_requests_.clear();
+}
+
+void GCMClientImpl::Register(
+ scoped_refptr<RegistrationInfo> registration_info) {
+ DCHECK_EQ(state_, READY);
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+
+ // Registrations should never pass as an app_id the special category used
+ // internally when registering with a subtype. See security note in
+ // GCMClientImpl::HandleIncomingMessage.
+ CHECK_NE(registration_info->app_id,
+ chrome_build_info_.product_category_for_subtypes);
+
+ // Find and use the cached registration ID.
+ RegistrationInfoMap::const_iterator registrations_iter =
+ registrations_.find(registration_info);
+ if (registrations_iter != registrations_.end()) {
+ bool matched = true;
+
+ // For GCM registration, we also match the sender IDs since multiple
+ // registrations are not supported.
+ const GCMRegistrationInfo* gcm_registration_info =
+ GCMRegistrationInfo::FromRegistrationInfo(registration_info.get());
+ if (gcm_registration_info) {
+ const GCMRegistrationInfo* cached_gcm_registration_info =
+ GCMRegistrationInfo::FromRegistrationInfo(
+ registrations_iter->first.get());
+ DCHECK(cached_gcm_registration_info);
+ if (gcm_registration_info->sender_ids !=
+ cached_gcm_registration_info->sender_ids) {
+ matched = false;
+ // Senders IDs don't match existing registration.
+ RecordRegistrationRequestToUMA(
+ RegistrationCacheStatus::REGISTRATION_FOUND_BUT_SENDERS_DONT_MATCH);
+ }
+ }
+
+ if (matched) {
+ // Skip registration if token is fresh.
+ base::TimeDelta token_invalidation_period =
+ features::GetTokenInvalidationInterval();
+ base::TimeDelta time_since_last_validation =
+ clock_->Now() - registrations_iter->first->last_validated;
+ if (token_invalidation_period.is_zero() ||
+ time_since_last_validation < token_invalidation_period) {
+ // Token is fresh, or token invalidation is disabled.
+ // Use cached registration.
+ delegate_->OnRegisterFinished(registration_info,
+ registrations_iter->second, SUCCESS);
+ RecordRegistrationRequestToUMA(
+ RegistrationCacheStatus::REGISTRATION_FOUND_AND_FRESH);
+ return;
+ } else {
+ // Token is stale.
+ RecordRegistrationRequestToUMA(
+ RegistrationCacheStatus::REGISTRATION_FOUND_BUT_STALE);
+ }
+ }
+ } else {
+ // New Registration request (no existing registration)
+ RecordRegistrationRequestToUMA(
+ RegistrationCacheStatus::REGISTRATION_NOT_FOUND);
+ }
+
+ std::unique_ptr<RegistrationRequest::CustomRequestHandler> request_handler;
+ std::string source_to_record;
+
+ const GCMRegistrationInfo* gcm_registration_info =
+ GCMRegistrationInfo::FromRegistrationInfo(registration_info.get());
+ if (gcm_registration_info) {
+ std::string senders;
+ for (auto iter = gcm_registration_info->sender_ids.begin();
+ iter != gcm_registration_info->sender_ids.end();
+ ++iter) {
+ if (!senders.empty())
+ senders.append(",");
+ senders.append(*iter);
+ }
+ request_handler = std::make_unique<GCMRegistrationRequestHandler>(senders);
+ source_to_record = senders;
+ }
+
+ const InstanceIDTokenInfo* instance_id_token_info =
+ InstanceIDTokenInfo::FromRegistrationInfo(registration_info.get());
+ if (instance_id_token_info) {
+ auto instance_id_iter = instance_id_data_.find(registration_info->app_id);
+ DCHECK(instance_id_iter != instance_id_data_.end());
+
+ request_handler = std::make_unique<InstanceIDGetTokenRequestHandler>(
+ instance_id_iter->second.first,
+ instance_id_token_info->authorized_entity,
+ instance_id_token_info->scope,
+ ConstructGCMVersion(chrome_build_info_.version),
+ instance_id_token_info->time_to_live);
+ source_to_record = instance_id_token_info->authorized_entity + "/" +
+ instance_id_token_info->scope;
+ }
+
+ bool use_subtype = instance_id_token_info &&
+ InstanceIDUsesSubtypeForAppId(registration_info->app_id);
+ std::string category = use_subtype
+ ? chrome_build_info_.product_category_for_subtypes
+ : registration_info->app_id;
+ std::string subtype = use_subtype ? registration_info->app_id : std::string();
+ RegistrationRequest::RequestInfo request_info(device_checkin_info_.android_id,
+ device_checkin_info_.secret,
+ category, subtype);
+
+ std::unique_ptr<RegistrationRequest> registration_request(
+ new RegistrationRequest(
+ gservices_settings_.GetRegistrationURL(), request_info,
+ std::move(request_handler), GetGCMBackoffPolicy(),
+ base::BindOnce(&GCMClientImpl::OnRegisterCompleted,
+ weak_ptr_factory_.GetWeakPtr(), registration_info),
+ kMaxRegistrationRetries, url_loader_factory_, io_task_runner_,
+ &recorder_, source_to_record));
+ registration_request->Start();
+ pending_registration_requests_.insert(
+ std::make_pair(registration_info, std::move(registration_request)));
+}
+
+void GCMClientImpl::OnRegisterCompleted(
+ scoped_refptr<RegistrationInfo> registration_info,
+ RegistrationRequest::Status status,
+ const std::string& registration_id) {
+ DCHECK(delegate_);
+
+ Result result;
+ PendingRegistrationRequests::const_iterator iter =
+ pending_registration_requests_.find(registration_info);
+ if (iter == pending_registration_requests_.end()) {
+ result = UNKNOWN_ERROR;
+ } else if (status == RegistrationRequest::INVALID_SENDER) {
+ result = INVALID_PARAMETER;
+ } else if (registration_id.empty()) {
+ // All other errors are currently treated as SERVER_ERROR (including
+ // REACHED_MAX_RETRIES due to the device being offline!).
+ result = SERVER_ERROR;
+ } else {
+ result = SUCCESS;
+ }
+
+ if (result == SUCCESS) {
+ // Cache it.
+ // Note that the existing cached record has to be removed first because
+ // otherwise the key value in registrations_ will not be updated. For GCM
+ // registrations, the key consists of pair of app_id and sender_ids though
+ // only app_id is used in the key comparison.
+ registrations_.erase(registration_info);
+ registration_info->last_validated = clock_->Now();
+ registrations_[registration_info] = registration_id;
+
+ // Save it in the persistent store.
+ gcm_store_->AddRegistration(
+ registration_info->GetSerializedKey(),
+ registration_info->GetSerializedValue(registration_id),
+ base::BindOnce(&GCMClientImpl::UpdateRegistrationCallback,
+ weak_ptr_factory_.GetWeakPtr()));
+ }
+
+ delegate_->OnRegisterFinished(
+ registration_info,
+ result == SUCCESS ? registration_id : std::string(),
+ result);
+
+ if (iter != pending_registration_requests_.end())
+ pending_registration_requests_.erase(iter);
+}
+
+bool GCMClientImpl::ValidateRegistration(
+ scoped_refptr<RegistrationInfo> registration_info,
+ const std::string& registration_id) {
+ DCHECK_EQ(state_, READY);
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+
+ // Must have a cached registration.
+ RegistrationInfoMap::const_iterator registrations_iter =
+ registrations_.find(registration_info);
+ if (registrations_iter == registrations_.end())
+ return false;
+
+ // Cached registration ID must match.
+ const std::string& cached_registration_id = registrations_iter->second;
+ if (registration_id != cached_registration_id)
+ return false;
+
+ // For GCM registration, we also match the sender IDs since multiple
+ // registrations are not supported.
+ const GCMRegistrationInfo* gcm_registration_info =
+ GCMRegistrationInfo::FromRegistrationInfo(registration_info.get());
+ if (gcm_registration_info) {
+ const GCMRegistrationInfo* cached_gcm_registration_info =
+ GCMRegistrationInfo::FromRegistrationInfo(
+ registrations_iter->first.get());
+ DCHECK(cached_gcm_registration_info);
+ if (gcm_registration_info->sender_ids !=
+ cached_gcm_registration_info->sender_ids) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void GCMClientImpl::Unregister(
+ scoped_refptr<RegistrationInfo> registration_info) {
+ DCHECK_EQ(state_, READY);
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+
+ std::unique_ptr<UnregistrationRequest::CustomRequestHandler> request_handler;
+ std::string source_to_record;
+
+ const GCMRegistrationInfo* gcm_registration_info =
+ GCMRegistrationInfo::FromRegistrationInfo(registration_info.get());
+ if (gcm_registration_info) {
+ request_handler = std::make_unique<GCMUnregistrationRequestHandler>(
+ registration_info->app_id);
+ }
+
+ const InstanceIDTokenInfo* instance_id_token_info =
+ InstanceIDTokenInfo::FromRegistrationInfo(registration_info.get());
+ if (instance_id_token_info) {
+ auto instance_id_iter = instance_id_data_.find(registration_info->app_id);
+ if (instance_id_iter == instance_id_data_.end()) {
+ // This should not be reached since we should not delete tokens when
+ // an InstanceID has not been created yet.
+ NOTREACHED();
+ return;
+ }
+
+ request_handler = std::make_unique<InstanceIDDeleteTokenRequestHandler>(
+ instance_id_iter->second.first,
+ instance_id_token_info->authorized_entity,
+ instance_id_token_info->scope,
+ ConstructGCMVersion(chrome_build_info_.version));
+ source_to_record = instance_id_token_info->authorized_entity + "/" +
+ instance_id_token_info->scope;
+ }
+
+ // Remove the registration/token(s) from the cache and the store.
+ // TODO(jianli): Remove it only when the request is successful.
+ if (instance_id_token_info &&
+ instance_id_token_info->authorized_entity == "*" &&
+ instance_id_token_info->scope == "*") {
+ // If authorized_entity and scope are '*', find and remove all associated
+ // tokens.
+ bool token_found = false;
+ for (auto iter = registrations_.begin();
+ iter != registrations_.end();) {
+ InstanceIDTokenInfo* cached_instance_id_token_info =
+ InstanceIDTokenInfo::FromRegistrationInfo(iter->first.get());
+ if (cached_instance_id_token_info &&
+ cached_instance_id_token_info->app_id == registration_info->app_id) {
+ token_found = true;
+ gcm_store_->RemoveRegistration(
+ cached_instance_id_token_info->GetSerializedKey(),
+ base::BindOnce(&GCMClientImpl::UpdateRegistrationCallback,
+ weak_ptr_factory_.GetWeakPtr()));
+ registrations_.erase(iter++);
+ } else {
+ ++iter;
+ }
+ }
+
+ // If no token is found for the Instance ID, don't need to unregister
+ // since the Instance ID is not sent to the server yet.
+ if (!token_found) {
+ OnUnregisterCompleted(registration_info,
+ UnregistrationRequest::SUCCESS);
+ return;
+ }
+ } else {
+ auto iter = registrations_.find(registration_info);
+ if (iter == registrations_.end()) {
+ delegate_->OnUnregisterFinished(registration_info, INVALID_PARAMETER);
+ return;
+ }
+ registrations_.erase(iter);
+
+ gcm_store_->RemoveRegistration(
+ registration_info->GetSerializedKey(),
+ base::BindOnce(&GCMClientImpl::UpdateRegistrationCallback,
+ weak_ptr_factory_.GetWeakPtr()));
+ }
+
+ bool use_subtype = instance_id_token_info &&
+ InstanceIDUsesSubtypeForAppId(registration_info->app_id);
+ std::string category = use_subtype
+ ? chrome_build_info_.product_category_for_subtypes
+ : registration_info->app_id;
+ std::string subtype = use_subtype ? registration_info->app_id : std::string();
+ UnregistrationRequest::RequestInfo request_info(
+ device_checkin_info_.android_id, device_checkin_info_.secret, category,
+ subtype);
+
+ std::unique_ptr<UnregistrationRequest> unregistration_request(
+ new UnregistrationRequest(
+ gservices_settings_.GetRegistrationURL(), request_info,
+ std::move(request_handler), GetGCMBackoffPolicy(),
+ base::BindOnce(&GCMClientImpl::OnUnregisterCompleted,
+ weak_ptr_factory_.GetWeakPtr(), registration_info),
+ kMaxUnregistrationRetries, url_loader_factory_, io_task_runner_,
+ &recorder_, source_to_record));
+ unregistration_request->Start();
+ pending_unregistration_requests_.insert(
+ std::make_pair(registration_info, std::move(unregistration_request)));
+}
+
+void GCMClientImpl::OnUnregisterCompleted(
+ scoped_refptr<RegistrationInfo> registration_info,
+ UnregistrationRequest::Status status) {
+ DVLOG(1) << "Unregister completed for app: " << registration_info->app_id
+ << " with " << (status ? "success." : "failure.");
+
+ Result result;
+ switch (status) {
+ case UnregistrationRequest::SUCCESS:
+ result = SUCCESS;
+ break;
+ case UnregistrationRequest::INVALID_PARAMETERS:
+ result = INVALID_PARAMETER;
+ break;
+ default:
+ // All other errors are currently treated as SERVER_ERROR (including
+ // REACHED_MAX_RETRIES due to the device being offline!).
+ result = SERVER_ERROR;
+ break;
+ }
+ delegate_->OnUnregisterFinished(registration_info, result);
+
+ pending_unregistration_requests_.erase(registration_info);
+}
+
+void GCMClientImpl::OnGCMStoreDestroyed(bool success) {
+ DLOG_IF(ERROR, !success) << "GCM store failed to be destroyed!";
+ UMA_HISTOGRAM_BOOLEAN("GCM.StoreDestroySucceeded", success);
+}
+
+void GCMClientImpl::Send(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) {
+ DCHECK_EQ(state_, READY);
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+
+ mcs_proto::DataMessageStanza stanza;
+ stanza.set_ttl(message.time_to_live);
+ stanza.set_sent(clock_->Now().ToInternalValue() /
+ base::Time::kMicrosecondsPerSecond);
+ stanza.set_id(message.id);
+ stanza.set_from(kSendMessageFromValue);
+ stanza.set_to(receiver_id);
+ stanza.set_category(app_id);
+
+ for (auto iter = message.data.begin(); iter != message.data.end(); ++iter) {
+ mcs_proto::AppData* app_data = stanza.add_app_data();
+ app_data->set_key(iter->first);
+ app_data->set_value(iter->second);
+ }
+
+ MCSMessage mcs_message(stanza);
+ DVLOG(1) << "MCS message size: " << mcs_message.size();
+ mcs_client_->SendMessage(mcs_message);
+}
+
+std::string GCMClientImpl::GetStateString() const {
+ switch(state_) {
+ case GCMClientImpl::UNINITIALIZED:
+ return "UNINITIALIZED";
+ case GCMClientImpl::INITIALIZED:
+ return "INITIALIZED";
+ case GCMClientImpl::LOADING:
+ return "LOADING";
+ case GCMClientImpl::LOADED:
+ return "LOADED";
+ case GCMClientImpl::INITIAL_DEVICE_CHECKIN:
+ return "INITIAL_DEVICE_CHECKIN";
+ case GCMClientImpl::READY:
+ return "READY";
+ }
+ NOTREACHED();
+ return std::string();
+}
+
+void GCMClientImpl::RecordDecryptionFailure(const std::string& app_id,
+ GCMDecryptionResult result) {
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+ recorder_.RecordDecryptionFailure(app_id, result);
+}
+
+void GCMClientImpl::SetRecording(bool recording) {
+ recorder_.set_is_recording(recording);
+}
+
+void GCMClientImpl::ClearActivityLogs() {
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+ recorder_.Clear();
+}
+
+GCMClient::GCMStatistics GCMClientImpl::GetStatistics() const {
+ DCHECK(io_task_runner_->RunsTasksInCurrentSequence());
+ GCMClient::GCMStatistics stats;
+ stats.gcm_client_created = true;
+ stats.is_recording = recorder_.is_recording();
+ stats.gcm_client_state = GetStateString();
+ stats.connection_client_created = mcs_client_ != nullptr;
+ stats.last_checkin = last_checkin_time_;
+ stats.next_checkin =
+ last_checkin_time_ + gservices_settings_.GetCheckinInterval();
+ if (connection_factory_)
+ stats.connection_state = connection_factory_->GetConnectionStateString();
+ if (mcs_client_) {
+ stats.send_queue_size = mcs_client_->GetSendQueueSize();
+ stats.resend_queue_size = mcs_client_->GetResendQueueSize();
+ }
+ if (device_checkin_info_.android_id > 0)
+ stats.android_id = device_checkin_info_.android_id;
+ if (device_checkin_info_.secret > 0)
+ stats.android_secret = device_checkin_info_.secret;
+
+ recorder_.CollectActivities(&stats.recorded_activities);
+
+ for (auto it = registrations_.begin(); it != registrations_.end(); ++it) {
+ stats.registered_app_ids.push_back(it->first->app_id);
+ }
+ return stats;
+}
+
+void GCMClientImpl::OnActivityRecorded() {
+ delegate_->OnActivityRecorded();
+}
+
+void GCMClientImpl::OnConnected(const GURL& current_server,
+ const net::IPEndPoint& ip_endpoint) {
+ // TODO(gcm): expose current server in debug page.
+ delegate_->OnActivityRecorded();
+ delegate_->OnConnected(ip_endpoint);
+}
+
+void GCMClientImpl::OnDisconnected() {
+ delegate_->OnActivityRecorded();
+ delegate_->OnDisconnected();
+}
+
+void GCMClientImpl::OnMessageReceivedFromMCS(const gcm::MCSMessage& message) {
+ switch (message.tag()) {
+ case kLoginResponseTag:
+ DVLOG(1) << "Login response received by GCM Client. Ignoring.";
+ return;
+ case kDataMessageStanzaTag:
+ DVLOG(1) << "A downstream message received. Processing...";
+ HandleIncomingMessage(message);
+ return;
+ default:
+ NOTREACHED() << "Message with unexpected tag received by GCMClient";
+ return;
+ }
+}
+
+void GCMClientImpl::OnMessageSentToMCS(int64_t user_serial_number,
+ const std::string& app_id,
+ const std::string& message_id,
+ MCSClient::MessageSendStatus status) {
+ DCHECK_EQ(user_serial_number, kDefaultUserSerialNumber);
+ DCHECK(delegate_);
+
+ // TTL_EXCEEDED is singled out here, because it can happen long time after the
+ // message was sent. That is why it comes as |OnMessageSendError| event rather
+ // than |OnSendFinished|. SendErrorDetails.additional_data is left empty.
+ // All other errors will be raised immediately, through asynchronous callback.
+ // It is expected that TTL_EXCEEDED will be issued for a message that was
+ // previously issued |OnSendFinished| with status SUCCESS.
+ // TODO(jianli): Consider adding UMA for this status.
+ if (status == MCSClient::TTL_EXCEEDED) {
+ SendErrorDetails send_error_details;
+ send_error_details.message_id = message_id;
+ send_error_details.result = GCMClient::TTL_EXCEEDED;
+ delegate_->OnMessageSendError(app_id, send_error_details);
+ } else if (status == MCSClient::SENT) {
+ delegate_->OnSendAcknowledged(app_id, message_id);
+ } else {
+ delegate_->OnSendFinished(app_id, message_id, ToGCMClientResult(status));
+ }
+}
+
+void GCMClientImpl::OnMCSError() {
+ // TODO(fgorski): For now it replaces the initialization method. Long term it
+ // should have an error or status passed in.
+}
+
+void GCMClientImpl::HandleIncomingMessage(const gcm::MCSMessage& message) {
+ DCHECK(delegate_);
+
+ const mcs_proto::DataMessageStanza& data_message_stanza =
+ reinterpret_cast<const mcs_proto::DataMessageStanza&>(
+ message.GetProtobuf());
+ DCHECK_EQ(data_message_stanza.device_user_id(), kDefaultUserSerialNumber);
+
+ // Copy all the data from the stanza to a MessageData object. When present,
+ // keys like kSubtypeKey or kMessageTypeKey will be filtered out later.
+ MessageData message_data;
+ for (int i = 0; i < data_message_stanza.app_data_size(); ++i) {
+ std::string key = data_message_stanza.app_data(i).key();
+ message_data[key] = data_message_stanza.app_data(i).value();
+ }
+
+ std::string subtype;
+ auto subtype_iter = message_data.find(kSubtypeKey);
+ if (subtype_iter != message_data.end()) {
+ subtype = subtype_iter->second;
+ message_data.erase(subtype_iter);
+ }
+
+ // SECURITY NOTE: Subtypes received from GCM *cannot* be trusted for
+ // registrations without a subtype (as the sender can pass any subtype they
+ // want). They can however be trusted for registrations that are known to have
+ // a subtype (as GCM overwrites anything passed by the sender).
+ //
+ // So a given Chrome profile always passes a fixed string called
+ // |product_category_for_subtypes| (of the form "com.chrome.macosx") as the
+ // category when registering with a subtype, and incoming subtypes are only
+ // trusted for that category.
+ //
+ // TODO(johnme): Remove this check if GCM starts sending the subtype in a
+ // field that's guaranteed to be trusted (b/18198485).
+ //
+ // (On Android, all registrations made by Chrome on behalf of third-party
+ // apps/extensions/websites have always had a subtype, so such a check is not
+ // necessary - or possible, since category is fixed to the true package name).
+ bool subtype_is_trusted = data_message_stanza.category() ==
+ chrome_build_info_.product_category_for_subtypes;
+ bool use_subtype = subtype_is_trusted && !subtype.empty();
+ std::string app_id = use_subtype ? subtype : data_message_stanza.category();
+
+ MessageType message_type = DATA_MESSAGE;
+ auto type_iter = message_data.find(kMessageTypeKey);
+ if (type_iter != message_data.end()) {
+ message_type = DecodeMessageType(type_iter->second);
+ message_data.erase(type_iter);
+ }
+
+ switch (message_type) {
+ case DATA_MESSAGE:
+ HandleIncomingDataMessage(app_id, use_subtype, data_message_stanza,
+ message_data);
+ break;
+ case DELETED_MESSAGES:
+ HandleIncomingDeletedMessages(app_id, data_message_stanza, message_data);
+ break;
+ case SEND_ERROR:
+ HandleIncomingSendError(app_id, data_message_stanza, message_data);
+ break;
+ case UNKNOWN:
+ DVLOG(1) << "Unknown message_type received. Message ignored. "
+ << "App ID: " << app_id << ".";
+ break;
+ }
+}
+
+void GCMClientImpl::HandleIncomingDataMessage(
+ const std::string& app_id,
+ bool was_subtype,
+ const mcs_proto::DataMessageStanza& data_message_stanza,
+ MessageData& message_data) {
+ UMA_HISTOGRAM_BOOLEAN("GCM.DataMessageReceived", true);
+
+ bool has_collapse_key =
+ data_message_stanza.has_token() && !data_message_stanza.token().empty();
+ UMA_HISTOGRAM_BOOLEAN("GCM.DataMessageReceivedHasCollapseKey",
+ has_collapse_key);
+
+ recorder_.RecordDataMessageReceived(app_id, data_message_stanza.from(),
+ data_message_stanza.ByteSize(),
+ GCMStatsRecorder::DATA_MESSAGE);
+
+ IncomingMessage incoming_message;
+ incoming_message.sender_id = data_message_stanza.from();
+ incoming_message.message_id = data_message_stanza.persistent_id();
+ if (data_message_stanza.has_token())
+ incoming_message.collapse_key = data_message_stanza.token();
+ incoming_message.data = message_data;
+ incoming_message.raw_data = data_message_stanza.raw_data();
+
+ delegate_->OnMessageReceived(app_id, incoming_message);
+}
+
+void GCMClientImpl::HandleIncomingDeletedMessages(
+ const std::string& app_id,
+ const mcs_proto::DataMessageStanza& data_message_stanza,
+ MessageData& message_data) {
+ int deleted_count = 0;
+ auto count_iter = message_data.find(kDeletedCountKey);
+ if (count_iter != message_data.end()) {
+ if (!base::StringToInt(count_iter->second, &deleted_count))
+ deleted_count = 0;
+ }
+ UMA_HISTOGRAM_COUNTS_1000("GCM.DeletedMessagesReceived", deleted_count);
+
+ recorder_.RecordDataMessageReceived(app_id, data_message_stanza.from(),
+ data_message_stanza.ByteSize(),
+ GCMStatsRecorder::DELETED_MESSAGES);
+ delegate_->OnMessagesDeleted(app_id);
+}
+
+void GCMClientImpl::HandleIncomingSendError(
+ const std::string& app_id,
+ const mcs_proto::DataMessageStanza& data_message_stanza,
+ MessageData& message_data) {
+ SendErrorDetails send_error_details;
+ send_error_details.additional_data = message_data;
+ send_error_details.result = SERVER_ERROR;
+ send_error_details.message_id = data_message_stanza.persistent_id();
+
+ recorder_.RecordIncomingSendError(app_id, data_message_stanza.to(),
+ data_message_stanza.id());
+ delegate_->OnMessageSendError(app_id, send_error_details);
+}
+
+bool GCMClientImpl::HasStandaloneRegisteredApp() const {
+ if (registrations_.empty())
+ return false;
+ // Note that account mapper is not counted as a standalone app since it is
+ // automatically started when other app uses GCM.
+ return registrations_.size() > 1 ||
+ !ExistsGCMRegistrationInMap(registrations_, kGCMAccountMapperAppId);
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_client_impl.h b/chromium/components/gcm_driver/gcm_client_impl.h
new file mode 100644
index 00000000000..c393383a537
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_client_impl.h
@@ -0,0 +1,430 @@
+// Copyright 2014 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 COMPONENTS_GCM_DRIVER_GCM_CLIENT_IMPL_H_
+#define COMPONENTS_GCM_DRIVER_GCM_CLIENT_IMPL_H_
+
+#include <stdint.h>
+
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/compiler_specific.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "base/timer/timer.h"
+#include "components/gcm_driver/gcm_client.h"
+#include "components/gcm_driver/gcm_stats_recorder_impl.h"
+#include "google_apis/gcm/base/mcs_message.h"
+#include "google_apis/gcm/engine/gcm_store.h"
+#include "google_apis/gcm/engine/gservices_settings.h"
+#include "google_apis/gcm/engine/mcs_client.h"
+#include "google_apis/gcm/engine/registration_request.h"
+#include "google_apis/gcm/engine/unregistration_request.h"
+#include "google_apis/gcm/protocol/android_checkin.pb.h"
+#include "google_apis/gcm/protocol/checkin.pb.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "net/http/http_status_code.h"
+#include "services/network/public/mojom/proxy_resolving_socket.mojom.h"
+
+class GURL;
+
+namespace base {
+class Clock;
+class Time;
+} // namespace base
+
+namespace mcs_proto {
+class DataMessageStanza;
+} // namespace mcs_proto
+
+namespace network {
+class NetworkConnectionTracker;
+class SharedURLLoaderFactory;
+} // namespace network
+
+namespace gcm {
+
+class CheckinRequest;
+class ConnectionFactory;
+class GCMClientImplTest;
+
+// Helper class for building GCM internals. Allows tests to inject fake versions
+// as necessary.
+class GCMInternalsBuilder {
+ public:
+ GCMInternalsBuilder();
+ virtual ~GCMInternalsBuilder();
+
+ virtual base::Clock* GetClock();
+ virtual std::unique_ptr<MCSClient> BuildMCSClient(
+ const std::string& version,
+ base::Clock* clock,
+ ConnectionFactory* connection_factory,
+ GCMStore* gcm_store,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner,
+ GCMStatsRecorder* recorder);
+ virtual std::unique_ptr<ConnectionFactory> BuildConnectionFactory(
+ const std::vector<GURL>& endpoints,
+ const net::BackoffEntry::Policy& backoff_policy,
+ base::RepeatingCallback<void(
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)>
+ get_socket_factory_callback,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner,
+ GCMStatsRecorder* recorder,
+ network::NetworkConnectionTracker* network_connection_tracker);
+};
+
+// Implements the GCM Client. It is used to coordinate MCS Client (communication
+// with MCS) and other pieces of GCM infrastructure like Registration and
+// Checkins. It also allows for registering user delegates that host
+// applications that send and receive messages.
+class GCMClientImpl
+ : public GCMClient, public GCMStatsRecorder::Delegate,
+ public ConnectionFactory::ConnectionListener {
+ public:
+ // State representation of the GCMClient.
+ // Any change made to this enum should have corresponding change in the
+ // GetStateString(...) function.
+ enum State {
+ // Uninitialized.
+ UNINITIALIZED,
+ // Initialized,
+ INITIALIZED,
+ // GCM store loading is in progress.
+ LOADING,
+ // GCM store is loaded.
+ LOADED,
+ // Initial device checkin is in progress.
+ INITIAL_DEVICE_CHECKIN,
+ // Ready to accept requests.
+ READY,
+ };
+
+ explicit GCMClientImpl(
+ std::unique_ptr<GCMInternalsBuilder> internals_builder);
+
+ GCMClientImpl(const GCMClientImpl&) = delete;
+ GCMClientImpl& operator=(const GCMClientImpl&) = delete;
+
+ ~GCMClientImpl() override;
+
+ // GCMClient implementation.
+ void Initialize(
+ const ChromeBuildInfo& chrome_build_info,
+ const base::FilePath& store_path,
+ bool remove_account_mappings_with_email_key,
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner,
+ base::RepeatingCallback<void(
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)>
+ get_socket_factory_callback,
+ const scoped_refptr<network::SharedURLLoaderFactory>& url_loader_factory,
+ network::NetworkConnectionTracker* network_connection_tracker,
+ std::unique_ptr<Encryptor> encryptor,
+ GCMClient::Delegate* delegate) override;
+ void Start(StartMode start_mode) override;
+ void Stop() override;
+ void Register(scoped_refptr<RegistrationInfo> registration_info) override;
+ bool ValidateRegistration(scoped_refptr<RegistrationInfo> registration_info,
+ const std::string& registration_id) override;
+ void Unregister(scoped_refptr<RegistrationInfo> registration_info) override;
+ void Send(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) override;
+ void RecordDecryptionFailure(const std::string& app_id,
+ GCMDecryptionResult result) override;
+ void SetRecording(bool recording) override;
+ void ClearActivityLogs() override;
+ GCMStatistics GetStatistics() const override;
+ void SetAccountTokens(
+ const std::vector<AccountTokenInfo>& account_tokens) override;
+ void UpdateAccountMapping(const AccountMapping& account_mapping) override;
+ void RemoveAccountMapping(const CoreAccountId& account_id) override;
+ void SetLastTokenFetchTime(const base::Time& time) override;
+ void UpdateHeartbeatTimer(
+ std::unique_ptr<base::RetainingOneShotTimer> timer) override;
+ void AddInstanceIDData(const std::string& app_id,
+ const std::string& instance_id,
+ const std::string& extra_data) override;
+ void RemoveInstanceIDData(const std::string& app_id) override;
+ void GetInstanceIDData(const std::string& app_id,
+ std::string* instance_id,
+ std::string* extra_data) override;
+ void AddHeartbeatInterval(const std::string& scope, int interval_ms) override;
+ void RemoveHeartbeatInterval(const std::string& scope) override;
+
+ // GCMStatsRecorder::Delegate implemenation.
+ void OnActivityRecorded() override;
+
+ // ConnectionFactory::ConnectionListener implementation.
+ void OnConnected(const GURL& current_server,
+ const net::IPEndPoint& ip_endpoint) override;
+ void OnDisconnected() override;
+
+ private:
+ // The check-in info for the device.
+ // TODO(fgorski): Convert to a class with explicit getters/setters.
+ struct CheckinInfo {
+ CheckinInfo();
+ ~CheckinInfo();
+ bool IsValid() const { return android_id != 0 && secret != 0; }
+ void SnapshotCheckinAccounts();
+ void Reset();
+
+ // Android ID of the device as assigned by the server.
+ uint64_t android_id;
+ // Security token of the device as assigned by the server.
+ uint64_t secret;
+ // True if accounts were already provided through SetAccountsForCheckin(),
+ // or when |last_checkin_accounts| was loaded as empty.
+ bool accounts_set;
+ // Map of account email addresses and OAuth2 tokens that will be sent to the
+ // checkin server on a next checkin.
+ std::map<std::string, std::string> account_tokens;
+ // As set of accounts last checkin was completed with.
+ std::set<std::string> last_checkin_accounts;
+ };
+
+ // Reasons for resetting the GCM Store.
+ // Note: this enum is recorded into a histogram. Do not change enum value
+ // or order.
+ enum ResetReason {
+ LOAD_FAILURE, // GCM store failed to load, but the store exists.
+ CHECKIN_REJECTED, // Checkin was rejected by server.
+
+ RESET_REASON_COUNT,
+ };
+
+ // Collection of pending registration requests. Keys are RegistrationInfo
+ // instance, while values are pending registration requests to obtain a
+ // registration ID for requesting application.
+ using PendingRegistrationRequests =
+ std::map<scoped_refptr<RegistrationInfo>,
+ std::unique_ptr<RegistrationRequest>,
+ RegistrationInfoComparer>;
+
+ // Collection of pending unregistration requests. Keys are RegistrationInfo
+ // instance, while values are pending unregistration requests to disable the
+ // registration ID currently assigned to the application.
+ using PendingUnregistrationRequests =
+ std::map<scoped_refptr<RegistrationInfo>,
+ std::unique_ptr<UnregistrationRequest>,
+ RegistrationInfoComparer>;
+
+ friend class GCMClientImplTest;
+ friend class GCMClientInstanceIDTest;
+
+ // Returns text representation of the enum State.
+ std::string GetStateString() const;
+
+ // Callbacks for the MCSClient.
+ // Receives messages and dispatches them to relevant user delegates.
+ void OnMessageReceivedFromMCS(const gcm::MCSMessage& message);
+ // Receives confirmation of sent messages or information about errors.
+ void OnMessageSentToMCS(int64_t user_serial_number,
+ const std::string& app_id,
+ const std::string& message_id,
+ MCSClient::MessageSendStatus status);
+ // Receives information about mcs_client_ errors.
+ void OnMCSError();
+
+ // Runs after GCM Store load is done to trigger continuation of the
+ // initialization.
+ void OnLoadCompleted(std::unique_ptr<GCMStore::LoadResult> result);
+ // Starts the GCM.
+ void StartGCM();
+ // Initializes mcs_client_, which handles the connection to MCS.
+ void InitializeMCSClient();
+ // Complets the first time device checkin.
+ void OnFirstTimeDeviceCheckinCompleted(const CheckinInfo& checkin_info);
+ // Starts a login on mcs_client_.
+ void StartMCSLogin();
+ // Resets the GCM store when it is corrupted.
+ void ResetStore();
+ // Sets state to ready. This will initiate the MCS login and notify the
+ // delegates.
+ void OnReady(const std::vector<AccountMapping>& account_mappings,
+ const base::Time& last_token_fetch_time);
+
+ // Starts a first time device checkin.
+ void StartCheckin();
+ // Completes the device checkin request by parsing the |checkin_response|.
+ // Function also cleans up the pending checkin.
+ void OnCheckinCompleted(
+ net::HttpStatusCode response_code,
+ const checkin_proto::AndroidCheckinResponse& checkin_response);
+
+ // Callback passed to GCMStore::SetGServicesSettings.
+ void SetGServicesSettingsCallback(bool success);
+
+ // Schedules next periodic device checkin and makes sure there is at most one
+ // pending checkin at a time. This function is meant to be called after a
+ // successful checkin.
+ void SchedulePeriodicCheckin();
+ // Gets the time until next checkin.
+ base::TimeDelta GetTimeToNextCheckin() const;
+ // Callback for setting last checkin information in the |gcm_store_|.
+ void SetLastCheckinInfoCallback(bool success);
+
+ // Callback for persisting device credentials in the |gcm_store_|.
+ void SetDeviceCredentialsCallback(bool success);
+
+ // Callback for persisting registration info in the |gcm_store_|.
+ void UpdateRegistrationCallback(bool success);
+
+ // Callback for all store operations that do not try to recover, if write in
+ // |gcm_store_| fails.
+ void DefaultStoreCallback(bool success);
+
+ // Callback for store operation where result does not matter.
+ void IgnoreWriteResultCallback(const std::string& operation_suffix_for_uma,
+ bool success);
+
+ // Callback for destroying the GCM store.
+ void DestroyStoreCallback(bool success);
+
+ // Callback for resetting the GCM store. The store will be reloaded.
+ void ResetStoreCallback(bool success);
+
+ // Completes the registration request.
+ void OnRegisterCompleted(scoped_refptr<RegistrationInfo> registration_info,
+ RegistrationRequest::Status status,
+ const std::string& registration_id);
+
+ // Completes the unregistration request.
+ void OnUnregisterCompleted(scoped_refptr<RegistrationInfo> registration_info,
+ UnregistrationRequest::Status status);
+
+ // Completes the GCM store destroy request.
+ void OnGCMStoreDestroyed(bool success);
+
+ // Handles incoming data message and dispatches it the delegate of this class.
+ void HandleIncomingMessage(const gcm::MCSMessage& message);
+
+ // Fires OnMessageReceived event on the delegate of this class, based on the
+ // details in |data_message_stanza| and |message_data|.
+ void HandleIncomingDataMessage(
+ const std::string& app_id,
+ bool was_subtype,
+ const mcs_proto::DataMessageStanza& data_message_stanza,
+ MessageData& message_data);
+
+ // Fires OnMessagesDeleted event on the delegate of this class, based on the
+ // details in |data_message_stanza| and |message_data|.
+ void HandleIncomingDeletedMessages(
+ const std::string& app_id,
+ const mcs_proto::DataMessageStanza& data_message_stanza,
+ MessageData& message_data);
+
+ // Fires OnMessageSendError event on the delegate of this class, based on the
+ // details in |data_message_stanza| and |message_data|.
+ void HandleIncomingSendError(
+ const std::string& app_id,
+ const mcs_proto::DataMessageStanza& data_message_stanza,
+ MessageData& message_data);
+
+ // Is there any standalone app being registered for GCM?
+ bool HasStandaloneRegisteredApp() const;
+
+ // Destroys the store when it is not needed.
+ void DestroyStoreWhenNotNeeded();
+
+ // Reset all cahced values.
+ void ResetCache();
+
+ // Builder for the GCM internals (mcs client, etc.).
+ std::unique_ptr<GCMInternalsBuilder> internals_builder_;
+
+ // Recorder that logs GCM activities.
+ GCMStatsRecorderImpl recorder_;
+
+ // State of the GCM Client Implementation.
+ State state_;
+
+ raw_ptr<GCMClient::Delegate> delegate_;
+
+ // Flag to indicate if the GCM should be delay started until it is actually
+ // used in either of the following cases:
+ // 1) The GCM store contains the registration records.
+ // 2) GCM functionailities are explicitly called.
+ StartMode start_mode_;
+
+ // Device checkin info (android ID and security token used by device).
+ CheckinInfo device_checkin_info_;
+
+ // Clock used for timing of retry logic. Passed in for testing.
+ raw_ptr<base::Clock> clock_;
+
+ // Information about the chrome build.
+ // TODO(fgorski): Check if it can be passed in constructor and made const.
+ ChromeBuildInfo chrome_build_info_;
+
+ // Persistent data store for keeping device credentials, messages and user to
+ // serial number mappings.
+ std::unique_ptr<GCMStore> gcm_store_;
+
+ // Data loaded from the GCM store.
+ std::unique_ptr<GCMStore::LoadResult> load_result_;
+
+ // Tracks if the GCM store has been reset. This is used to prevent from
+ // resetting and loading from the store again and again.
+ bool gcm_store_reset_;
+
+ std::unique_ptr<ConnectionFactory> connection_factory_;
+ base::RepeatingCallback<void(
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)>
+ get_socket_factory_callback_;
+
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
+
+ raw_ptr<network::NetworkConnectionTracker> network_connection_tracker_;
+
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner_;
+
+ // Controls receiving and sending of packets and reliable message queueing.
+ // Must be destroyed before |network_session_|.
+ std::unique_ptr<MCSClient> mcs_client_;
+
+ std::unique_ptr<CheckinRequest> checkin_request_;
+
+ // Cached registration info.
+ RegistrationInfoMap registrations_;
+
+ // Currently pending registration requests. GCMClientImpl owns the
+ // RegistrationRequests.
+ PendingRegistrationRequests pending_registration_requests_;
+
+ // Currently pending unregistration requests. GCMClientImpl owns the
+ // UnregistrationRequests.
+ PendingUnregistrationRequests pending_unregistration_requests_;
+
+ // G-services settings that were provided by MCS.
+ GServicesSettings gservices_settings_;
+
+ // Time of the last successful checkin.
+ base::Time last_checkin_time_;
+
+ // Cached instance ID data, key is app ID and value is pair of instance ID
+ // and extra data.
+ std::map<std::string, std::pair<std::string, std::string>> instance_id_data_;
+
+ // Factory for creating references when scheduling periodic checkin.
+ base::WeakPtrFactory<GCMClientImpl> periodic_checkin_ptr_factory_{this};
+
+ // Factory for wiping out GCM store.
+ base::WeakPtrFactory<GCMClientImpl> destroying_gcm_store_ptr_factory_{this};
+
+ // Factory for creating references in callbacks.
+ base::WeakPtrFactory<GCMClientImpl> weak_ptr_factory_{this};
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_CLIENT_IMPL_H_
diff --git a/chromium/components/gcm_driver/gcm_client_impl_unittest.cc b/chromium/components/gcm_driver/gcm_client_impl_unittest.cc
new file mode 100644
index 00000000000..9492e9105e6
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_client_impl_unittest.cc
@@ -0,0 +1,1966 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/gcm_client_impl.h"
+
+#include <stdint.h>
+
+#include <initializer_list>
+#include <memory>
+
+#include "base/callback_helpers.h"
+#include "base/command_line.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/memory/ptr_util.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/test/task_environment.h"
+#include "base/time/clock.h"
+#include "base/timer/timer.h"
+#include "components/gcm_driver/features.h"
+#include "google_apis/gcm/base/fake_encryptor.h"
+#include "google_apis/gcm/base/mcs_message.h"
+#include "google_apis/gcm/base/mcs_util.h"
+#include "google_apis/gcm/engine/fake_connection_factory.h"
+#include "google_apis/gcm/engine/fake_connection_handler.h"
+#include "google_apis/gcm/engine/gservices_settings.h"
+#include "google_apis/gcm/monitoring/gcm_stats_recorder.h"
+#include "google_apis/gcm/protocol/android_checkin.pb.h"
+#include "google_apis/gcm/protocol/checkin.pb.h"
+#include "google_apis/gcm/protocol/mcs.pb.h"
+#include "net/test/gtest_util.h"
+#include "net/test/scoped_disable_exit_on_dfatal.h"
+#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
+#include "services/network/public/mojom/url_response_head.mojom.h"
+#include "services/network/test/test_network_connection_tracker.h"
+#include "services/network/test/test_url_loader_factory.h"
+#include "services/network/test/test_utils.h"
+#include "testing/gtest/include/gtest/gtest-spi.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/leveldatabase/leveldb_chrome.h"
+
+namespace gcm {
+namespace {
+
+enum LastEvent {
+ NONE,
+ LOADING_COMPLETED,
+ REGISTRATION_COMPLETED,
+ UNREGISTRATION_COMPLETED,
+ MESSAGE_SEND_ERROR,
+ MESSAGE_SEND_ACK,
+ MESSAGE_RECEIVED,
+ MESSAGES_DELETED,
+};
+
+const char kChromeVersion[] = "45.0.0.1";
+const uint64_t kDeviceAndroidId = 54321;
+const uint64_t kDeviceSecurityToken = 12345;
+const uint64_t kDeviceAndroidId2 = 11111;
+const uint64_t kDeviceSecurityToken2 = 2222;
+const int64_t kSettingsCheckinInterval = 16 * 60 * 60;
+const char kProductCategoryForSubtypes[] = "com.chrome.macosx";
+const char kExtensionAppId[] = "abcdefghijklmnopabcdefghijklmnop";
+const char kRegistrationId[] = "reg_id";
+const char kSubtypeAppId[] = "app_id";
+const char kSender[] = "project_id";
+const char kSender2[] = "project_id2";
+const char kRegistrationResponsePrefix[] = "token=";
+const char kUnregistrationResponsePrefix[] = "deleted=";
+const char kRawData[] = "example raw data";
+
+const char kInstanceID[] = "iid_1";
+const char kScope[] = "GCM";
+const char kDeleteTokenResponse[] = "token=foo";
+const int kTestTokenInvalidationPeriod = 5;
+const char kMessageId[] = "0:12345%5678";
+
+const char kRegisterUrl[] = "https://android.clients.google.com/c2dm/register3";
+
+// Helper for building arbitrary data messages.
+MCSMessage BuildDownstreamMessage(
+ const std::string& project_id,
+ const std::string& category,
+ const std::string& subtype,
+ const std::map<std::string, std::string>& data,
+ const std::string& raw_data) {
+ mcs_proto::DataMessageStanza data_message;
+ data_message.set_from(project_id);
+ data_message.set_category(category);
+ for (auto iter = data.begin(); iter != data.end(); ++iter) {
+ mcs_proto::AppData* app_data = data_message.add_app_data();
+ app_data->set_key(iter->first);
+ app_data->set_value(iter->second);
+ }
+ if (!subtype.empty()) {
+ mcs_proto::AppData* app_data = data_message.add_app_data();
+ app_data->set_key("subtype");
+ app_data->set_value(subtype);
+ }
+ data_message.set_raw_data(raw_data);
+ data_message.set_persistent_id(kMessageId);
+ return MCSMessage(kDataMessageStanzaTag, data_message);
+}
+
+GCMClient::AccountTokenInfo MakeAccountToken(const std::string& email,
+ const std::string& token) {
+ GCMClient::AccountTokenInfo account_token;
+ account_token.email = email;
+ account_token.access_token = token;
+ return account_token;
+}
+
+std::map<std::string, std::string> MakeEmailToTokenMap(
+ const std::vector<GCMClient::AccountTokenInfo>& account_tokens) {
+ std::map<std::string, std::string> email_token_map;
+ for (auto iter = account_tokens.begin(); iter != account_tokens.end();
+ ++iter) {
+ email_token_map[iter->email] = iter->access_token;
+ }
+ return email_token_map;
+}
+
+class FakeMCSClient : public MCSClient {
+ public:
+ FakeMCSClient(base::Clock* clock,
+ ConnectionFactory* connection_factory,
+ GCMStore* gcm_store,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner,
+ GCMStatsRecorder* recorder);
+ ~FakeMCSClient() override;
+ void Login(uint64_t android_id, uint64_t security_token) override;
+ void SendMessage(const MCSMessage& message) override;
+
+ uint64_t last_android_id() const { return last_android_id_; }
+ uint64_t last_security_token() const { return last_security_token_; }
+ uint8_t last_message_tag() const { return last_message_tag_; }
+ const mcs_proto::DataMessageStanza& last_data_message_stanza() const {
+ return last_data_message_stanza_;
+ }
+
+ private:
+ uint64_t last_android_id_;
+ uint64_t last_security_token_;
+ uint8_t last_message_tag_;
+ mcs_proto::DataMessageStanza last_data_message_stanza_;
+};
+
+FakeMCSClient::FakeMCSClient(
+ base::Clock* clock,
+ ConnectionFactory* connection_factory,
+ GCMStore* gcm_store,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner,
+ GCMStatsRecorder* recorder)
+ : MCSClient("",
+ clock,
+ connection_factory,
+ gcm_store,
+ io_task_runner,
+ recorder),
+ last_android_id_(0u),
+ last_security_token_(0u),
+ last_message_tag_(kNumProtoTypes) {}
+
+FakeMCSClient::~FakeMCSClient() {
+}
+
+void FakeMCSClient::Login(uint64_t android_id, uint64_t security_token) {
+ last_android_id_ = android_id;
+ last_security_token_ = security_token;
+}
+
+void FakeMCSClient::SendMessage(const MCSMessage& message) {
+ last_message_tag_ = message.tag();
+ if (last_message_tag_ == kDataMessageStanzaTag) {
+ last_data_message_stanza_.CopyFrom(
+ reinterpret_cast<const mcs_proto::DataMessageStanza&>(
+ message.GetProtobuf()));
+ }
+}
+
+class AutoAdvancingTestClock : public base::Clock {
+ public:
+ explicit AutoAdvancingTestClock(base::TimeDelta auto_increment_time_delta);
+
+ AutoAdvancingTestClock(const AutoAdvancingTestClock&) = delete;
+ AutoAdvancingTestClock& operator=(const AutoAdvancingTestClock&) = delete;
+
+ ~AutoAdvancingTestClock() override;
+
+ base::Time Now() const override;
+ void Advance(base::TimeDelta delta);
+ int call_count() const { return call_count_; }
+
+ private:
+ mutable int call_count_;
+ base::TimeDelta auto_increment_time_delta_;
+ mutable base::Time now_;
+};
+
+AutoAdvancingTestClock::AutoAdvancingTestClock(
+ base::TimeDelta auto_increment_time_delta)
+ : call_count_(0), auto_increment_time_delta_(auto_increment_time_delta) {
+}
+
+AutoAdvancingTestClock::~AutoAdvancingTestClock() {
+}
+
+base::Time AutoAdvancingTestClock::Now() const {
+ call_count_++;
+ now_ += auto_increment_time_delta_;
+ return now_;
+}
+
+void AutoAdvancingTestClock::Advance(base::TimeDelta delta) {
+ now_ += delta;
+}
+
+class FakeGCMInternalsBuilder : public GCMInternalsBuilder {
+ public:
+ explicit FakeGCMInternalsBuilder(base::TimeDelta clock_step);
+ ~FakeGCMInternalsBuilder() override;
+
+ base::Clock* GetClock() override;
+ std::unique_ptr<MCSClient> BuildMCSClient(
+ const std::string& version,
+ base::Clock* clock,
+ ConnectionFactory* connection_factory,
+ GCMStore* gcm_store,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner,
+ GCMStatsRecorder* recorder) override;
+ std::unique_ptr<ConnectionFactory> BuildConnectionFactory(
+ const std::vector<GURL>& endpoints,
+ const net::BackoffEntry::Policy& backoff_policy,
+ base::RepeatingCallback<void(
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)>
+ get_socket_factory_callback,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner,
+ GCMStatsRecorder* recorder,
+ network::NetworkConnectionTracker* network_connection_tracker) override;
+
+ private:
+ AutoAdvancingTestClock clock_;
+};
+
+FakeGCMInternalsBuilder::FakeGCMInternalsBuilder(base::TimeDelta clock_step)
+ : clock_(clock_step) {}
+
+FakeGCMInternalsBuilder::~FakeGCMInternalsBuilder() {}
+
+base::Clock* FakeGCMInternalsBuilder::GetClock() {
+ return &clock_;
+}
+
+std::unique_ptr<MCSClient> FakeGCMInternalsBuilder::BuildMCSClient(
+ const std::string& version,
+ base::Clock* clock,
+ ConnectionFactory* connection_factory,
+ GCMStore* gcm_store,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner,
+ GCMStatsRecorder* recorder) {
+ return base::WrapUnique<MCSClient>(new FakeMCSClient(
+ clock, connection_factory, gcm_store, io_task_runner, recorder));
+}
+
+std::unique_ptr<ConnectionFactory>
+FakeGCMInternalsBuilder::BuildConnectionFactory(
+ const std::vector<GURL>& endpoints,
+ const net::BackoffEntry::Policy& backoff_policy,
+ base::RepeatingCallback<void(
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)>
+ get_socket_factory_callback,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner,
+ GCMStatsRecorder* recorder,
+ network::NetworkConnectionTracker* network_connection_tracker) {
+ return base::WrapUnique<ConnectionFactory>(new FakeConnectionFactory());
+}
+
+} // namespace
+
+class GCMClientImplTest : public testing::Test,
+ public GCMClient::Delegate {
+ public:
+ GCMClientImplTest();
+ ~GCMClientImplTest() override;
+
+ void SetUp() override;
+ void TearDown() override;
+
+ void SetFeatureParams(const base::Feature& feature,
+ const base::FieldTrialParams& params);
+
+ void InitializeInvalidationFieldTrial();
+
+ void BuildGCMClient(base::TimeDelta clock_step);
+ void InitializeGCMClient();
+ void StartGCMClient();
+ void Register(const std::string& app_id,
+ const std::vector<std::string>& senders);
+ void Unregister(const std::string& app_id);
+ void ReceiveMessageFromMCS(const MCSMessage& message);
+ void ReceiveOnMessageSentToMCS(
+ const std::string& app_id,
+ const std::string& message_id,
+ const MCSClient::MessageSendStatus status);
+ void FailCheckin(net::HttpStatusCode response_code);
+ void CompleteCheckin(uint64_t android_id,
+ uint64_t security_token,
+ const std::string& digest,
+ const std::map<std::string, std::string>& settings);
+ void CompleteCheckinImpl(uint64_t android_id,
+ uint64_t security_token,
+ const std::string& digest,
+ const std::map<std::string, std::string>& settings,
+ net::HttpStatusCode response_code);
+ void CompleteRegistration(const std::string& registration_id);
+ void CompleteUnregistration(const std::string& app_id);
+
+ bool ExistsRegistration(const std::string& app_id) const;
+ void AddRegistration(const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ const std::string& registration_id);
+
+ // GCMClient::Delegate overrides (for verification).
+ void OnRegisterFinished(scoped_refptr<RegistrationInfo> registration_info,
+ const std::string& registration_id,
+ GCMClient::Result result) override;
+ void OnUnregisterFinished(scoped_refptr<RegistrationInfo> registration_info,
+ GCMClient::Result result) override;
+ void OnSendFinished(const std::string& app_id,
+ const std::string& message_id,
+ GCMClient::Result result) override {}
+ void OnMessageReceived(const std::string& registration_id,
+ const IncomingMessage& message) override;
+ void OnMessagesDeleted(const std::string& app_id) override;
+ void OnMessageSendError(
+ const std::string& app_id,
+ const gcm::GCMClient::SendErrorDetails& send_error_details) override;
+ void OnSendAcknowledged(const std::string& app_id,
+ const std::string& message_id) override;
+ void OnGCMReady(const std::vector<AccountMapping>& account_mappings,
+ const base::Time& last_token_fetch_time) override;
+ void OnActivityRecorded() override {}
+ void OnConnected(const net::IPEndPoint& ip_endpoint) override {}
+ void OnDisconnected() override {}
+ void OnStoreReset() override {}
+
+ GCMClientImpl* gcm_client() const { return gcm_client_.get(); }
+ GCMClientImpl::State gcm_client_state() const {
+ return gcm_client_->state_;
+ }
+ FakeMCSClient* mcs_client() const {
+ return static_cast<FakeMCSClient*>(gcm_client_->mcs_client_.get());
+ }
+ ConnectionFactory* connection_factory() const {
+ return gcm_client_->connection_factory_.get();
+ }
+
+ const GCMClientImpl::CheckinInfo& device_checkin_info() const {
+ return gcm_client_->device_checkin_info_;
+ }
+
+ void reset_last_event() {
+ last_event_ = NONE;
+ last_app_id_.clear();
+ last_registration_id_.clear();
+ last_message_id_.clear();
+ last_result_ = GCMClient::UNKNOWN_ERROR;
+ last_account_mappings_.clear();
+ last_token_fetch_time_ = base::Time();
+ }
+
+ LastEvent last_event() const { return last_event_; }
+ const std::string& last_app_id() const { return last_app_id_; }
+ const std::string& last_registration_id() const {
+ return last_registration_id_;
+ }
+ const std::string& last_message_id() const { return last_message_id_; }
+ GCMClient::Result last_result() const { return last_result_; }
+ const IncomingMessage& last_message() const { return last_message_; }
+ const GCMClient::SendErrorDetails& last_error_details() const {
+ return last_error_details_;
+ }
+ const base::Time& last_token_fetch_time() const {
+ return last_token_fetch_time_;
+ }
+ const std::vector<AccountMapping>& last_account_mappings() {
+ return last_account_mappings_;
+ }
+
+ const GServicesSettings& gservices_settings() const {
+ return gcm_client_->gservices_settings_;
+ }
+
+ const base::FilePath& temp_directory_path() const {
+ return temp_directory_.GetPath();
+ }
+
+ base::FilePath gcm_store_path() const {
+ // Pass an non-existent directory as store path to match the exact
+ // behavior in the production code. Currently GCMStoreImpl checks if
+ // the directory exist or not to determine the store existence.
+ return temp_directory_.GetPath().Append(FILE_PATH_LITERAL("GCM Store"));
+ }
+
+ int64_t CurrentTime();
+
+ // Tooling.
+ void PumpLoopUntilIdle();
+ bool CreateUniqueTempDir();
+ AutoAdvancingTestClock* clock() const {
+ return static_cast<AutoAdvancingTestClock*>(gcm_client_->clock_);
+ }
+ network::TestURLLoaderFactory* url_loader_factory() {
+ return &test_url_loader_factory_;
+ }
+
+ void FastForwardBy(const base::TimeDelta& duration) {
+ task_environment_.FastForwardBy(duration);
+ }
+
+ private:
+ base::test::TaskEnvironment task_environment_{
+ base::test::TaskEnvironment::TimeSource::MOCK_TIME};
+
+ // Must be declared first so that it is destroyed last. Injected to
+ // GCM client.
+ base::ScopedTempDir temp_directory_;
+
+ // Variables used for verification.
+ LastEvent last_event_;
+ std::string last_app_id_;
+ std::string last_registration_id_;
+ std::string last_message_id_;
+ GCMClient::Result last_result_;
+ IncomingMessage last_message_;
+ GCMClient::SendErrorDetails last_error_details_;
+ base::Time last_token_fetch_time_;
+ std::vector<AccountMapping> last_account_mappings_;
+
+ std::unique_ptr<GCMClientImpl> gcm_client_;
+
+ // Injected to GCM client.
+ network::TestURLLoaderFactory test_url_loader_factory_;
+ base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+GCMClientImplTest::GCMClientImplTest()
+ : last_event_(NONE), last_result_(GCMClient::UNKNOWN_ERROR) {}
+
+GCMClientImplTest::~GCMClientImplTest() {}
+
+void GCMClientImplTest::SetUp() {
+ testing::Test::SetUp();
+ ASSERT_TRUE(CreateUniqueTempDir());
+ BuildGCMClient(base::TimeDelta());
+ InitializeGCMClient();
+ StartGCMClient();
+ InitializeInvalidationFieldTrial();
+ ASSERT_NO_FATAL_FAILURE(
+ CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken, std::string(),
+ std::map<std::string, std::string>()));
+}
+
+void GCMClientImplTest::TearDown() {
+ gcm_client_.reset();
+ PumpLoopUntilIdle();
+ testing::Test::TearDown();
+}
+
+void GCMClientImplTest::SetFeatureParams(const base::Feature& feature,
+ const base::FieldTrialParams& params) {
+ scoped_feature_list_.InitAndEnableFeatureWithParameters(feature, params);
+
+ base::FieldTrialParams actual_params;
+ EXPECT_TRUE(base::GetFieldTrialParamsByFeature(
+ features::kInvalidateTokenFeature, &actual_params));
+ EXPECT_EQ(params, actual_params);
+}
+
+void GCMClientImplTest::InitializeInvalidationFieldTrial() {
+ std::map<std::string, std::string> params;
+ params[features::kParamNameTokenInvalidationPeriodDays] =
+ std::to_string(kTestTokenInvalidationPeriod);
+ ASSERT_NO_FATAL_FAILURE(
+ SetFeatureParams(features::kInvalidateTokenFeature, std::move(params)));
+}
+
+void GCMClientImplTest::PumpLoopUntilIdle() {
+ task_environment_.RunUntilIdle();
+}
+
+bool GCMClientImplTest::CreateUniqueTempDir() {
+ return temp_directory_.CreateUniqueTempDir();
+}
+
+void GCMClientImplTest::BuildGCMClient(base::TimeDelta clock_step) {
+ gcm_client_ =
+ std::make_unique<GCMClientImpl>(base::WrapUnique<GCMInternalsBuilder>(
+ new FakeGCMInternalsBuilder(clock_step)));
+}
+
+void GCMClientImplTest::FailCheckin(net::HttpStatusCode response_code) {
+ std::map<std::string, std::string> settings;
+ CompleteCheckinImpl(0, 0, GServicesSettings::CalculateDigest(settings),
+ settings, response_code);
+}
+
+void GCMClientImplTest::CompleteCheckin(
+ uint64_t android_id,
+ uint64_t security_token,
+ const std::string& digest,
+ const std::map<std::string, std::string>& settings) {
+ CompleteCheckinImpl(android_id, security_token, digest, settings,
+ net::HTTP_OK);
+}
+
+void GCMClientImplTest::CompleteCheckinImpl(
+ uint64_t android_id,
+ uint64_t security_token,
+ const std::string& digest,
+ const std::map<std::string, std::string>& settings,
+ net::HttpStatusCode response_code) {
+ checkin_proto::AndroidCheckinResponse response;
+ response.set_stats_ok(true);
+ response.set_android_id(android_id);
+ response.set_security_token(security_token);
+
+ // For testing G-services settings.
+ if (!digest.empty()) {
+ response.set_digest(digest);
+ for (auto it = settings.begin(); it != settings.end(); ++it) {
+ checkin_proto::GservicesSetting* setting = response.add_setting();
+ setting->set_name(it->first);
+ setting->set_value(it->second);
+ }
+ response.set_settings_diff(false);
+ }
+
+ std::string response_string;
+ response.SerializeToString(&response_string);
+
+ EXPECT_TRUE(url_loader_factory()->SimulateResponseForPendingRequest(
+ gservices_settings().GetCheckinURL(),
+ network::URLLoaderCompletionStatus(net::OK),
+ network::CreateURLResponseHead(response_code), response_string));
+ // Give a chance for GCMStoreImpl::Backend to finish persisting data.
+ PumpLoopUntilIdle();
+}
+
+void GCMClientImplTest::CompleteRegistration(
+ const std::string& registration_id) {
+ std::string response(kRegistrationResponsePrefix);
+ response.append(registration_id);
+
+ EXPECT_TRUE(url_loader_factory()->SimulateResponseForPendingRequest(
+ GURL(kRegisterUrl), network::URLLoaderCompletionStatus(net::OK),
+ network::CreateURLResponseHead(net::HTTP_OK), response));
+
+ // Give a chance for GCMStoreImpl::Backend to finish persisting data.
+ PumpLoopUntilIdle();
+}
+
+void GCMClientImplTest::CompleteUnregistration(
+ const std::string& app_id) {
+ std::string response(kUnregistrationResponsePrefix);
+ response.append(app_id);
+
+ EXPECT_TRUE(url_loader_factory()->SimulateResponseForPendingRequest(
+ GURL(kRegisterUrl), network::URLLoaderCompletionStatus(net::OK),
+ network::CreateURLResponseHead(net::HTTP_OK), response));
+
+ // Give a chance for GCMStoreImpl::Backend to finish persisting data.
+ PumpLoopUntilIdle();
+}
+
+bool GCMClientImplTest::ExistsRegistration(const std::string& app_id) const {
+ return ExistsGCMRegistrationInMap(gcm_client_->registrations_, app_id);
+}
+
+void GCMClientImplTest::AddRegistration(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ const std::string& registration_id) {
+ auto registration = base::MakeRefCounted<GCMRegistrationInfo>();
+ registration->app_id = app_id;
+ registration->sender_ids = sender_ids;
+ gcm_client_->registrations_.emplace(std::move(registration), registration_id);
+}
+
+void GCMClientImplTest::InitializeGCMClient() {
+ clock()->Advance(base::Milliseconds(1));
+
+ // Actual initialization.
+ GCMClient::ChromeBuildInfo chrome_build_info;
+ chrome_build_info.version = kChromeVersion;
+ chrome_build_info.product_category_for_subtypes = kProductCategoryForSubtypes;
+ gcm_client_->Initialize(
+ chrome_build_info, gcm_store_path(),
+ /*remove_account_mappings_with_email_key=*/true,
+ task_environment_.GetMainThreadTaskRunner(),
+ base::ThreadTaskRunnerHandle::Get(), base::DoNothing(),
+ base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
+ &test_url_loader_factory_),
+ network::TestNetworkConnectionTracker::GetInstance(),
+ base::WrapUnique<Encryptor>(new FakeEncryptor), this);
+}
+
+void GCMClientImplTest::StartGCMClient() {
+ // Start loading and check-in.
+ gcm_client_->Start(GCMClient::IMMEDIATE_START);
+
+ PumpLoopUntilIdle();
+}
+
+void GCMClientImplTest::Register(const std::string& app_id,
+ const std::vector<std::string>& senders) {
+ auto gcm_info = base::MakeRefCounted<GCMRegistrationInfo>();
+ gcm_info->app_id = app_id;
+ gcm_info->sender_ids = senders;
+ gcm_client()->Register(std::move(gcm_info));
+}
+
+void GCMClientImplTest::Unregister(const std::string& app_id) {
+ auto gcm_info = base::MakeRefCounted<GCMRegistrationInfo>();
+ gcm_info->app_id = app_id;
+ gcm_client()->Unregister(std::move(gcm_info));
+}
+
+void GCMClientImplTest::ReceiveMessageFromMCS(const MCSMessage& message) {
+ gcm_client_->recorder_.RecordConnectionInitiated(std::string());
+ gcm_client_->recorder_.RecordConnectionSuccess();
+ gcm_client_->OnMessageReceivedFromMCS(message);
+}
+
+void GCMClientImplTest::ReceiveOnMessageSentToMCS(
+ const std::string& app_id,
+ const std::string& message_id,
+ const MCSClient::MessageSendStatus status) {
+ gcm_client_->OnMessageSentToMCS(0LL, app_id, message_id, status);
+}
+
+void GCMClientImplTest::OnGCMReady(
+ const std::vector<AccountMapping>& account_mappings,
+ const base::Time& last_token_fetch_time) {
+ last_event_ = LOADING_COMPLETED;
+ last_account_mappings_ = account_mappings;
+ last_token_fetch_time_ = last_token_fetch_time;
+}
+
+void GCMClientImplTest::OnMessageReceived(const std::string& registration_id,
+ const IncomingMessage& message) {
+ last_event_ = MESSAGE_RECEIVED;
+ last_app_id_ = registration_id;
+ last_message_ = message;
+}
+
+void GCMClientImplTest::OnRegisterFinished(
+ scoped_refptr<RegistrationInfo> registration_info,
+ const std::string& registration_id,
+ GCMClient::Result result) {
+ last_event_ = REGISTRATION_COMPLETED;
+ last_app_id_ = registration_info->app_id;
+ last_registration_id_ = registration_id;
+ last_result_ = result;
+}
+
+void GCMClientImplTest::OnUnregisterFinished(
+ scoped_refptr<RegistrationInfo> registration_info,
+ GCMClient::Result result) {
+ last_event_ = UNREGISTRATION_COMPLETED;
+ last_app_id_ = registration_info->app_id;
+ last_result_ = result;
+}
+
+void GCMClientImplTest::OnMessagesDeleted(const std::string& app_id) {
+ last_event_ = MESSAGES_DELETED;
+ last_app_id_ = app_id;
+}
+
+void GCMClientImplTest::OnMessageSendError(
+ const std::string& app_id,
+ const gcm::GCMClient::SendErrorDetails& send_error_details) {
+ last_event_ = MESSAGE_SEND_ERROR;
+ last_app_id_ = app_id;
+ last_error_details_ = send_error_details;
+}
+
+void GCMClientImplTest::OnSendAcknowledged(const std::string& app_id,
+ const std::string& message_id) {
+ last_event_ = MESSAGE_SEND_ACK;
+ last_app_id_ = app_id;
+ last_message_id_ = message_id;
+}
+
+int64_t GCMClientImplTest::CurrentTime() {
+ return clock()->Now().ToInternalValue() / base::Time::kMicrosecondsPerSecond;
+}
+
+TEST_F(GCMClientImplTest, LoadingCompleted) {
+ EXPECT_EQ(LOADING_COMPLETED, last_event());
+ EXPECT_EQ(kDeviceAndroidId, mcs_client()->last_android_id());
+ EXPECT_EQ(kDeviceSecurityToken, mcs_client()->last_security_token());
+
+ // Checking freshly loaded CheckinInfo.
+ EXPECT_EQ(kDeviceAndroidId, device_checkin_info().android_id);
+ EXPECT_EQ(kDeviceSecurityToken, device_checkin_info().secret);
+ EXPECT_TRUE(device_checkin_info().last_checkin_accounts.empty());
+ EXPECT_TRUE(device_checkin_info().accounts_set);
+ EXPECT_TRUE(device_checkin_info().account_tokens.empty());
+}
+
+TEST_F(GCMClientImplTest, LoadingBusted) {
+ // Close the GCM store.
+ gcm_client()->Stop();
+ PumpLoopUntilIdle();
+
+ // Mess up the store.
+ EXPECT_TRUE(leveldb_chrome::CorruptClosedDBForTesting(gcm_store_path()));
+
+ // Restart GCM client. The store should be reset and the loading should
+ // complete successfully.
+ reset_last_event();
+ BuildGCMClient(base::TimeDelta());
+ InitializeGCMClient();
+ StartGCMClient();
+ ASSERT_NO_FATAL_FAILURE(
+ CompleteCheckin(kDeviceAndroidId2, kDeviceSecurityToken2, std::string(),
+ std::map<std::string, std::string>()));
+
+ EXPECT_EQ(LOADING_COMPLETED, last_event());
+ EXPECT_EQ(kDeviceAndroidId2, mcs_client()->last_android_id());
+ EXPECT_EQ(kDeviceSecurityToken2, mcs_client()->last_security_token());
+}
+
+TEST_F(GCMClientImplTest, LoadingWithEmptyDirectory) {
+ // Close the GCM store.
+ gcm_client()->Stop();
+ PumpLoopUntilIdle();
+
+ // Make the store directory empty, to simulate a previous destroy store
+ // operation failing to delete the store directory.
+ ASSERT_TRUE(base::DeletePathRecursively(gcm_store_path()));
+ ASSERT_TRUE(base::CreateDirectory(gcm_store_path()));
+
+ base::HistogramTester histogram_tester;
+
+ // Restart GCM client. The store should be considered to not exist.
+ BuildGCMClient(base::TimeDelta());
+ InitializeGCMClient();
+ gcm_client()->Start(GCMClient::DELAYED_START);
+ PumpLoopUntilIdle();
+ histogram_tester.ExpectUniqueSample("GCM.LoadStatus",
+ 13 /* STORE_DOES_NOT_EXIST */, 1);
+ // Since the store does not exist, the database should not have been opened.
+ histogram_tester.ExpectTotalCount("GCM.Database.Open", 0);
+ // Without a store, DELAYED_START loading should only reach INITIALIZED state.
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+
+ // The store directory should still exist (and be empty). If not, then the
+ // DELAYED_START load has probably reset the store, rather than leaving that
+ // to the next IMMEDIATE_START load as expected.
+ ASSERT_TRUE(base::DirectoryExists(gcm_store_path()));
+ ASSERT_FALSE(
+ base::PathExists(gcm_store_path().Append(FILE_PATH_LITERAL("CURRENT"))));
+
+ // IMMEDIATE_START loading should successfully create a new store despite the
+ // empty directory.
+ reset_last_event();
+ StartGCMClient();
+ ASSERT_NO_FATAL_FAILURE(
+ CompleteCheckin(kDeviceAndroidId2, kDeviceSecurityToken2, std::string(),
+ std::map<std::string, std::string>()));
+ EXPECT_EQ(LOADING_COMPLETED, last_event());
+ EXPECT_EQ(GCMClientImpl::READY, gcm_client_state());
+ EXPECT_EQ(kDeviceAndroidId2, mcs_client()->last_android_id());
+ EXPECT_EQ(kDeviceSecurityToken2, mcs_client()->last_security_token());
+}
+
+TEST_F(GCMClientImplTest, DestroyStoreWhenNotNeeded) {
+ // Close the GCM store.
+ gcm_client()->Stop();
+ PumpLoopUntilIdle();
+
+ // Restart GCM client. The store is loaded successfully.
+ reset_last_event();
+ BuildGCMClient(base::TimeDelta());
+ InitializeGCMClient();
+ gcm_client()->Start(GCMClient::DELAYED_START);
+ PumpLoopUntilIdle();
+
+ EXPECT_EQ(GCMClientImpl::LOADED, gcm_client_state());
+ EXPECT_TRUE(device_checkin_info().android_id);
+ EXPECT_TRUE(device_checkin_info().secret);
+
+ // Fast forward the clock to trigger the store destroying logic.
+ FastForwardBy(base::Milliseconds(300000));
+ PumpLoopUntilIdle();
+
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+ EXPECT_FALSE(device_checkin_info().android_id);
+ EXPECT_FALSE(device_checkin_info().secret);
+}
+
+TEST_F(GCMClientImplTest, SerializeAndDeserialize) {
+ std::vector<std::string> senders{"sender"};
+ auto gcm_info = base::MakeRefCounted<GCMRegistrationInfo>();
+ gcm_info->app_id = kExtensionAppId;
+ gcm_info->sender_ids = senders;
+ gcm_info->last_validated = clock()->Now();
+
+ auto gcm_info_deserialized = base::MakeRefCounted<GCMRegistrationInfo>();
+ std::string gcm_registration_id_deserialized;
+ {
+ std::string serialized_key = gcm_info->GetSerializedKey();
+ std::string serialized_value =
+ gcm_info->GetSerializedValue(kRegistrationId);
+
+ ASSERT_TRUE(gcm_info_deserialized->Deserialize(
+ serialized_key, serialized_value, &gcm_registration_id_deserialized));
+ }
+
+ EXPECT_EQ(gcm_info->app_id, gcm_info_deserialized->app_id);
+ EXPECT_EQ(gcm_info->sender_ids, gcm_info_deserialized->sender_ids);
+ EXPECT_EQ(gcm_info->last_validated, gcm_info_deserialized->last_validated);
+ EXPECT_EQ(kRegistrationId, gcm_registration_id_deserialized);
+
+ auto instance_id_info = base::MakeRefCounted<InstanceIDTokenInfo>();
+ instance_id_info->app_id = kExtensionAppId;
+ instance_id_info->last_validated = clock()->Now();
+ instance_id_info->authorized_entity = "different_sender";
+ instance_id_info->scope = "scope";
+
+ auto instance_id_info_deserialized =
+ base::MakeRefCounted<InstanceIDTokenInfo>();
+ std::string instance_id_registration_id_deserialized;
+ {
+ std::string serialized_key = instance_id_info->GetSerializedKey();
+ std::string serialized_value =
+ instance_id_info->GetSerializedValue(kRegistrationId);
+
+ ASSERT_TRUE(instance_id_info_deserialized->Deserialize(
+ serialized_key, serialized_value,
+ &instance_id_registration_id_deserialized));
+ }
+
+ EXPECT_EQ(instance_id_info->app_id, instance_id_info_deserialized->app_id);
+ EXPECT_EQ(instance_id_info->last_validated,
+ instance_id_info_deserialized->last_validated);
+ EXPECT_EQ(instance_id_info->authorized_entity,
+ instance_id_info_deserialized->authorized_entity);
+ EXPECT_EQ(instance_id_info->scope, instance_id_info_deserialized->scope);
+ EXPECT_EQ(kRegistrationId, instance_id_registration_id_deserialized);
+}
+
+TEST_F(GCMClientImplTest, RegisterApp) {
+ EXPECT_FALSE(ExistsRegistration(kExtensionAppId));
+
+ std::vector<std::string> senders;
+ senders.push_back("sender");
+ Register(kExtensionAppId, senders);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id"));
+
+ EXPECT_EQ(REGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ("reg_id", last_registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ EXPECT_TRUE(ExistsRegistration(kExtensionAppId));
+}
+
+TEST_F(GCMClientImplTest, RegisterAppFromCache) {
+ EXPECT_FALSE(ExistsRegistration(kExtensionAppId));
+
+ std::vector<std::string> senders;
+ senders.push_back("sender");
+ Register(kExtensionAppId, senders);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id"));
+ EXPECT_TRUE(ExistsRegistration(kExtensionAppId));
+
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ("reg_id", last_registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ EXPECT_EQ(REGISTRATION_COMPLETED, last_event());
+
+ // Recreate GCMClient in order to load from the persistent store.
+ BuildGCMClient(base::TimeDelta());
+ InitializeGCMClient();
+ StartGCMClient();
+
+ EXPECT_TRUE(ExistsRegistration(kExtensionAppId));
+}
+
+TEST_F(GCMClientImplTest, RegisterPreviousSenderAgain) {
+ EXPECT_FALSE(ExistsRegistration(kExtensionAppId));
+
+ // Register a sender.
+ std::vector<std::string> senders;
+ senders.push_back("sender");
+ Register(kExtensionAppId, senders);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id"));
+
+ EXPECT_EQ(REGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ("reg_id", last_registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ EXPECT_TRUE(ExistsRegistration(kExtensionAppId));
+
+ reset_last_event();
+
+ // Register a different sender. Different registration ID from previous one
+ // should be returned.
+ std::vector<std::string> senders2;
+ senders2.push_back("sender2");
+ Register(kExtensionAppId, senders2);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id2"));
+
+ EXPECT_EQ(REGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ("reg_id2", last_registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ EXPECT_TRUE(ExistsRegistration(kExtensionAppId));
+
+ reset_last_event();
+
+ // Register the 1st sender again. Different registration ID from previous one
+ // should be returned.
+ std::vector<std::string> senders3;
+ senders3.push_back("sender");
+ Register(kExtensionAppId, senders3);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id"));
+
+ EXPECT_EQ(REGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ("reg_id", last_registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ EXPECT_TRUE(ExistsRegistration(kExtensionAppId));
+}
+
+TEST_F(GCMClientImplTest, DISABLED_RegisterAgainWhenTokenIsFresh) {
+ // Register a sender.
+ std::vector<std::string> senders;
+ senders.push_back("sender");
+ Register(kExtensionAppId, senders);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id"));
+ EXPECT_EQ(REGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ("reg_id", last_registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ EXPECT_TRUE(ExistsRegistration(kExtensionAppId));
+
+ reset_last_event();
+
+ // Advance time by (kTestTokenInvalidationPeriod)/2
+ clock()->Advance(base::Days(kTestTokenInvalidationPeriod / 2));
+
+ // Register the same sender again. The same registration ID as the
+ // previous one should be returned, and we should *not* send a
+ // registration request to the GCM server.
+ Register(kExtensionAppId, senders);
+ PumpLoopUntilIdle();
+
+ EXPECT_EQ(REGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ("reg_id", last_registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ EXPECT_TRUE(ExistsRegistration(kExtensionAppId));
+}
+
+TEST_F(GCMClientImplTest, RegisterAgainWhenTokenIsStale) {
+ // Register a sender.
+ std::vector<std::string> senders;
+ senders.push_back("sender");
+ Register(kExtensionAppId, senders);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id"));
+
+ EXPECT_EQ(REGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ("reg_id", last_registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ EXPECT_TRUE(ExistsRegistration(kExtensionAppId));
+
+ reset_last_event();
+
+ // Advance time by kTestTokenInvalidationPeriod
+ clock()->Advance(base::Days(kTestTokenInvalidationPeriod));
+
+ // Register the same sender again. Different registration ID from the
+ // previous one should be returned.
+ Register(kExtensionAppId, senders);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id2"));
+
+ EXPECT_EQ(REGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ("reg_id2", last_registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ EXPECT_TRUE(ExistsRegistration(kExtensionAppId));
+}
+
+TEST_F(GCMClientImplTest, UnregisterApp) {
+ EXPECT_FALSE(ExistsRegistration(kExtensionAppId));
+
+ std::vector<std::string> senders;
+ senders.push_back("sender");
+ Register(kExtensionAppId, senders);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id"));
+ EXPECT_TRUE(ExistsRegistration(kExtensionAppId));
+
+ Unregister(kExtensionAppId);
+ ASSERT_NO_FATAL_FAILURE(CompleteUnregistration(kExtensionAppId));
+
+ EXPECT_EQ(UNREGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ EXPECT_FALSE(ExistsRegistration(kExtensionAppId));
+}
+
+// Tests that stopping the GCMClient also deletes pending registration requests.
+// This is tested by checking that url fetcher contained in the request was
+// deleted.
+TEST_F(GCMClientImplTest, DeletePendingRequestsWhenStopping) {
+ std::vector<std::string> senders;
+ senders.push_back("sender");
+ Register(kExtensionAppId, senders);
+
+ gcm_client()->Stop();
+ PumpLoopUntilIdle();
+ EXPECT_EQ(0, url_loader_factory()->NumPending());
+}
+
+TEST_F(GCMClientImplTest, DispatchDownstreamMessage) {
+ // Register to receive messages from kSender and kSender2 only.
+ std::vector<std::string> senders;
+ senders.push_back(kSender);
+ senders.push_back(kSender2);
+ AddRegistration(kExtensionAppId, senders, "reg_id");
+
+ std::map<std::string, std::string> expected_data;
+ expected_data["message_type"] = "gcm";
+ expected_data["key"] = "value";
+ expected_data["key2"] = "value2";
+
+ // Message for kSender will be received.
+ MCSMessage message(BuildDownstreamMessage(
+ kSender, kExtensionAppId, std::string() /* subtype */, expected_data,
+ std::string() /* raw_data */));
+ EXPECT_TRUE(message.IsValid());
+ ReceiveMessageFromMCS(message);
+
+ expected_data.erase(expected_data.find("message_type"));
+ EXPECT_EQ(MESSAGE_RECEIVED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ(expected_data.size(), last_message().data.size());
+ EXPECT_EQ(expected_data, last_message().data);
+ EXPECT_EQ(kSender, last_message().sender_id);
+
+ reset_last_event();
+
+ // Message for kSender2 will be received.
+ MCSMessage message2(BuildDownstreamMessage(
+ kSender2, kExtensionAppId, std::string() /* subtype */, expected_data,
+ std::string() /* raw_data */));
+ EXPECT_TRUE(message2.IsValid());
+ ReceiveMessageFromMCS(message2);
+
+ EXPECT_EQ(MESSAGE_RECEIVED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ(expected_data.size(), last_message().data.size());
+ EXPECT_EQ(expected_data, last_message().data);
+ EXPECT_EQ(kSender2, last_message().sender_id);
+}
+
+TEST_F(GCMClientImplTest, DispatchDownstreamMessageRawData) {
+ std::vector<std::string> senders(1, kSender);
+ AddRegistration(kExtensionAppId, senders, "reg_id");
+
+ std::map<std::string, std::string> expected_data;
+
+ MCSMessage message(BuildDownstreamMessage(kSender, kExtensionAppId,
+ std::string() /* subtype */,
+ expected_data, kRawData));
+ EXPECT_TRUE(message.IsValid());
+ ReceiveMessageFromMCS(message);
+
+ EXPECT_EQ(MESSAGE_RECEIVED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ(expected_data.size(), last_message().data.size());
+ EXPECT_EQ(kSender, last_message().sender_id);
+ EXPECT_EQ(kRawData, last_message().raw_data);
+}
+
+TEST_F(GCMClientImplTest, DISABLED_DispatchDownstreamMessageSendError) {
+ std::map<std::string, std::string> expected_data = {
+ {"message_type", "send_error"}, {"error_details", "some details"}};
+
+ MCSMessage message(BuildDownstreamMessage(
+ kSender, kExtensionAppId, std::string() /* subtype */, expected_data,
+ std::string() /* raw_data */));
+ EXPECT_TRUE(message.IsValid());
+ ReceiveMessageFromMCS(message);
+
+ EXPECT_EQ(MESSAGE_SEND_ERROR, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ(kMessageId, last_error_details().message_id);
+ EXPECT_EQ(1UL, last_error_details().additional_data.size());
+ auto iter = last_error_details().additional_data.find("error_details");
+ EXPECT_TRUE(iter != last_error_details().additional_data.end());
+ EXPECT_EQ("some details", iter->second);
+}
+
+TEST_F(GCMClientImplTest, DispatchDownstreamMessgaesDeleted) {
+ std::map<std::string, std::string> expected_data;
+ expected_data["message_type"] = "deleted_messages";
+ MCSMessage message(BuildDownstreamMessage(
+ kSender, kExtensionAppId, std::string() /* subtype */, expected_data,
+ std::string() /* raw_data */));
+ EXPECT_TRUE(message.IsValid());
+ ReceiveMessageFromMCS(message);
+
+ EXPECT_EQ(MESSAGES_DELETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+}
+
+TEST_F(GCMClientImplTest, SendMessage) {
+ OutgoingMessage message;
+ message.id = "007";
+ message.time_to_live = 500;
+ message.data["key"] = "value";
+ gcm_client()->Send(kExtensionAppId, kSender, message);
+
+ EXPECT_EQ(kDataMessageStanzaTag, mcs_client()->last_message_tag());
+ EXPECT_EQ(kExtensionAppId,
+ mcs_client()->last_data_message_stanza().category());
+ EXPECT_EQ(kSender, mcs_client()->last_data_message_stanza().to());
+ EXPECT_EQ(500, mcs_client()->last_data_message_stanza().ttl());
+ EXPECT_EQ(CurrentTime(), mcs_client()->last_data_message_stanza().sent());
+ EXPECT_EQ("007", mcs_client()->last_data_message_stanza().id());
+ EXPECT_EQ("gcm@chrome.com", mcs_client()->last_data_message_stanza().from());
+ EXPECT_EQ(kSender, mcs_client()->last_data_message_stanza().to());
+ EXPECT_EQ("key", mcs_client()->last_data_message_stanza().app_data(0).key());
+ EXPECT_EQ("value",
+ mcs_client()->last_data_message_stanza().app_data(0).value());
+}
+
+TEST_F(GCMClientImplTest, SendMessageAcknowledged) {
+ ReceiveOnMessageSentToMCS(kExtensionAppId, "007", MCSClient::SENT);
+ EXPECT_EQ(MESSAGE_SEND_ACK, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ("007", last_message_id());
+}
+
+class GCMClientImplCheckinTest : public GCMClientImplTest {
+ public:
+ GCMClientImplCheckinTest();
+ ~GCMClientImplCheckinTest() override;
+
+ void SetUp() override;
+};
+
+GCMClientImplCheckinTest::GCMClientImplCheckinTest() {
+}
+
+GCMClientImplCheckinTest::~GCMClientImplCheckinTest() {
+}
+
+void GCMClientImplCheckinTest::SetUp() {
+ testing::Test::SetUp();
+ // Creating unique temp directory that will be used by GCMStore shared between
+ // GCM Client and G-services settings.
+ ASSERT_TRUE(CreateUniqueTempDir());
+ // Time will be advancing one hour every time it is checked.
+ BuildGCMClient(base::Seconds(kSettingsCheckinInterval));
+ InitializeGCMClient();
+ StartGCMClient();
+}
+
+TEST_F(GCMClientImplCheckinTest, GServicesSettingsAfterInitialCheckin) {
+ std::map<std::string, std::string> settings;
+ settings["checkin_interval"] = base::NumberToString(kSettingsCheckinInterval);
+ settings["checkin_url"] = "http://alternative.url/checkin";
+ settings["gcm_hostname"] = "alternative.gcm.host";
+ settings["gcm_secure_port"] = "7777";
+ settings["gcm_registration_url"] = "http://alternative.url/registration";
+ ASSERT_NO_FATAL_FAILURE(
+ CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken,
+ GServicesSettings::CalculateDigest(settings), settings));
+ EXPECT_EQ(base::Seconds(kSettingsCheckinInterval),
+ gservices_settings().GetCheckinInterval());
+ EXPECT_EQ(GURL("http://alternative.url/checkin"),
+ gservices_settings().GetCheckinURL());
+ EXPECT_EQ(GURL("http://alternative.url/registration"),
+ gservices_settings().GetRegistrationURL());
+ EXPECT_EQ(GURL("https://alternative.gcm.host:7777"),
+ gservices_settings().GetMCSMainEndpoint());
+ EXPECT_EQ(GURL("https://alternative.gcm.host:443"),
+ gservices_settings().GetMCSFallbackEndpoint());
+}
+
+// This test only checks that periodic checkin happens.
+TEST_F(GCMClientImplCheckinTest, PeriodicCheckin) {
+ std::map<std::string, std::string> settings;
+ settings["checkin_interval"] = base::NumberToString(kSettingsCheckinInterval);
+ settings["checkin_url"] = "http://alternative.url/checkin";
+ settings["gcm_hostname"] = "alternative.gcm.host";
+ settings["gcm_secure_port"] = "7777";
+ settings["gcm_registration_url"] = "http://alternative.url/registration";
+ ASSERT_NO_FATAL_FAILURE(
+ CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken,
+ GServicesSettings::CalculateDigest(settings), settings));
+
+ EXPECT_EQ(2, clock()->call_count());
+
+ PumpLoopUntilIdle();
+ ASSERT_NO_FATAL_FAILURE(
+ CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken,
+ GServicesSettings::CalculateDigest(settings), settings));
+}
+
+TEST_F(GCMClientImplCheckinTest, LoadGSettingsFromStore) {
+ std::map<std::string, std::string> settings;
+ settings["checkin_interval"] = base::NumberToString(kSettingsCheckinInterval);
+ settings["checkin_url"] = "http://alternative.url/checkin";
+ settings["gcm_hostname"] = "alternative.gcm.host";
+ settings["gcm_secure_port"] = "7777";
+ settings["gcm_registration_url"] = "http://alternative.url/registration";
+ ASSERT_NO_FATAL_FAILURE(
+ CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken,
+ GServicesSettings::CalculateDigest(settings), settings));
+
+ BuildGCMClient(base::TimeDelta());
+ InitializeGCMClient();
+ StartGCMClient();
+
+ EXPECT_EQ(base::Seconds(kSettingsCheckinInterval),
+ gservices_settings().GetCheckinInterval());
+ EXPECT_EQ(GURL("http://alternative.url/checkin"),
+ gservices_settings().GetCheckinURL());
+ EXPECT_EQ(GURL("http://alternative.url/registration"),
+ gservices_settings().GetRegistrationURL());
+ EXPECT_EQ(GURL("https://alternative.gcm.host:7777"),
+ gservices_settings().GetMCSMainEndpoint());
+ EXPECT_EQ(GURL("https://alternative.gcm.host:443"),
+ gservices_settings().GetMCSFallbackEndpoint());
+}
+
+// This test only checks that periodic checkin happens.
+TEST_F(GCMClientImplCheckinTest, CheckinWithAccounts) {
+ std::map<std::string, std::string> settings;
+ settings["checkin_interval"] = base::NumberToString(kSettingsCheckinInterval);
+ settings["checkin_url"] = "http://alternative.url/checkin";
+ settings["gcm_hostname"] = "alternative.gcm.host";
+ settings["gcm_secure_port"] = "7777";
+ settings["gcm_registration_url"] = "http://alternative.url/registration";
+ ASSERT_NO_FATAL_FAILURE(
+ CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken,
+ GServicesSettings::CalculateDigest(settings), settings));
+
+ std::vector<GCMClient::AccountTokenInfo> account_tokens;
+ account_tokens.push_back(MakeAccountToken("test_user1@gmail.com", "token1"));
+ account_tokens.push_back(MakeAccountToken("test_user2@gmail.com", "token2"));
+ gcm_client()->SetAccountTokens(account_tokens);
+
+ EXPECT_TRUE(device_checkin_info().last_checkin_accounts.empty());
+ EXPECT_TRUE(device_checkin_info().accounts_set);
+ EXPECT_EQ(MakeEmailToTokenMap(account_tokens),
+ device_checkin_info().account_tokens);
+
+ PumpLoopUntilIdle();
+ ASSERT_NO_FATAL_FAILURE(
+ CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken,
+ GServicesSettings::CalculateDigest(settings), settings));
+
+ std::set<std::string> accounts;
+ accounts.insert("test_user1@gmail.com");
+ accounts.insert("test_user2@gmail.com");
+ EXPECT_EQ(accounts, device_checkin_info().last_checkin_accounts);
+ EXPECT_TRUE(device_checkin_info().accounts_set);
+ EXPECT_EQ(MakeEmailToTokenMap(account_tokens),
+ device_checkin_info().account_tokens);
+}
+
+// This test only checks that periodic checkin happens.
+TEST_F(GCMClientImplCheckinTest, CheckinWhenAccountRemoved) {
+ std::map<std::string, std::string> settings;
+ settings["checkin_interval"] = base::NumberToString(kSettingsCheckinInterval);
+ settings["checkin_url"] = "http://alternative.url/checkin";
+ settings["gcm_hostname"] = "alternative.gcm.host";
+ settings["gcm_secure_port"] = "7777";
+ settings["gcm_registration_url"] = "http://alternative.url/registration";
+ ASSERT_NO_FATAL_FAILURE(
+ CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken,
+ GServicesSettings::CalculateDigest(settings), settings));
+
+ std::vector<GCMClient::AccountTokenInfo> account_tokens;
+ account_tokens.push_back(MakeAccountToken("test_user1@gmail.com", "token1"));
+ account_tokens.push_back(MakeAccountToken("test_user2@gmail.com", "token2"));
+ gcm_client()->SetAccountTokens(account_tokens);
+ PumpLoopUntilIdle();
+ ASSERT_NO_FATAL_FAILURE(
+ CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken,
+ GServicesSettings::CalculateDigest(settings), settings));
+
+ EXPECT_EQ(2UL, device_checkin_info().last_checkin_accounts.size());
+ EXPECT_TRUE(device_checkin_info().accounts_set);
+ EXPECT_EQ(MakeEmailToTokenMap(account_tokens),
+ device_checkin_info().account_tokens);
+
+ account_tokens.erase(account_tokens.begin() + 1);
+ gcm_client()->SetAccountTokens(account_tokens);
+
+ PumpLoopUntilIdle();
+ ASSERT_NO_FATAL_FAILURE(
+ CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken,
+ GServicesSettings::CalculateDigest(settings), settings));
+
+ std::set<std::string> accounts;
+ accounts.insert("test_user1@gmail.com");
+ EXPECT_EQ(accounts, device_checkin_info().last_checkin_accounts);
+ EXPECT_TRUE(device_checkin_info().accounts_set);
+ EXPECT_EQ(MakeEmailToTokenMap(account_tokens),
+ device_checkin_info().account_tokens);
+}
+
+// This test only checks that periodic checkin happens.
+TEST_F(GCMClientImplCheckinTest, CheckinWhenAccountReplaced) {
+ std::map<std::string, std::string> settings;
+ settings["checkin_interval"] = base::NumberToString(kSettingsCheckinInterval);
+ settings["checkin_url"] = "http://alternative.url/checkin";
+ settings["gcm_hostname"] = "alternative.gcm.host";
+ settings["gcm_secure_port"] = "7777";
+ settings["gcm_registration_url"] = "http://alternative.url/registration";
+ ASSERT_NO_FATAL_FAILURE(
+ CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken,
+ GServicesSettings::CalculateDigest(settings), settings));
+
+ std::vector<GCMClient::AccountTokenInfo> account_tokens;
+ account_tokens.push_back(MakeAccountToken("test_user1@gmail.com", "token1"));
+ gcm_client()->SetAccountTokens(account_tokens);
+
+ PumpLoopUntilIdle();
+ ASSERT_NO_FATAL_FAILURE(
+ CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken,
+ GServicesSettings::CalculateDigest(settings), settings));
+
+ std::set<std::string> accounts;
+ accounts.insert("test_user1@gmail.com");
+ EXPECT_EQ(accounts, device_checkin_info().last_checkin_accounts);
+
+ // This should trigger another checkin, because the list of accounts is
+ // different.
+ account_tokens.clear();
+ account_tokens.push_back(MakeAccountToken("test_user2@gmail.com", "token2"));
+ gcm_client()->SetAccountTokens(account_tokens);
+
+ PumpLoopUntilIdle();
+ ASSERT_NO_FATAL_FAILURE(
+ CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken,
+ GServicesSettings::CalculateDigest(settings), settings));
+
+ accounts.clear();
+ accounts.insert("test_user2@gmail.com");
+ EXPECT_EQ(accounts, device_checkin_info().last_checkin_accounts);
+ EXPECT_TRUE(device_checkin_info().accounts_set);
+ EXPECT_EQ(MakeEmailToTokenMap(account_tokens),
+ device_checkin_info().account_tokens);
+}
+
+TEST_F(GCMClientImplCheckinTest, ResetStoreWhenCheckinRejected) {
+ base::HistogramTester histogram_tester;
+ std::map<std::string, std::string> settings;
+ ASSERT_NO_FATAL_FAILURE(FailCheckin(net::HTTP_UNAUTHORIZED));
+ PumpLoopUntilIdle();
+
+ // Store should have been destroyed. Restart client and verify the initial
+ // checkin response is persisted.
+ BuildGCMClient(base::TimeDelta());
+ InitializeGCMClient();
+ StartGCMClient();
+ ASSERT_NO_FATAL_FAILURE(
+ CompleteCheckin(kDeviceAndroidId2, kDeviceSecurityToken2,
+ GServicesSettings::CalculateDigest(settings), settings));
+
+ EXPECT_EQ(LOADING_COMPLETED, last_event());
+ EXPECT_EQ(kDeviceAndroidId2, mcs_client()->last_android_id());
+ EXPECT_EQ(kDeviceSecurityToken2, mcs_client()->last_security_token());
+}
+
+class GCMClientImplStartAndStopTest : public GCMClientImplTest {
+ public:
+ GCMClientImplStartAndStopTest();
+ ~GCMClientImplStartAndStopTest() override;
+
+ void SetUp() override;
+
+ void DefaultCompleteCheckin();
+};
+
+GCMClientImplStartAndStopTest::GCMClientImplStartAndStopTest() {
+}
+
+GCMClientImplStartAndStopTest::~GCMClientImplStartAndStopTest() {
+}
+
+void GCMClientImplStartAndStopTest::SetUp() {
+ testing::Test::SetUp();
+ ASSERT_TRUE(CreateUniqueTempDir());
+ BuildGCMClient(base::TimeDelta());
+ InitializeGCMClient();
+}
+
+void GCMClientImplStartAndStopTest::DefaultCompleteCheckin() {
+ ASSERT_NO_FATAL_FAILURE(
+ CompleteCheckin(kDeviceAndroidId, kDeviceSecurityToken, std::string(),
+ std::map<std::string, std::string>()));
+ PumpLoopUntilIdle();
+}
+
+TEST_F(GCMClientImplStartAndStopTest, DISABLED_StartStopAndRestart) {
+ // GCMClientImpl should be in INITIALIZED state at first.
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+
+ // Delay start the GCM.
+ gcm_client()->Start(GCMClient::DELAYED_START);
+ PumpLoopUntilIdle();
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+
+ // Stop the GCM.
+ gcm_client()->Stop();
+ PumpLoopUntilIdle();
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+
+ // Restart the GCM without delay.
+ gcm_client()->Start(GCMClient::IMMEDIATE_START);
+ PumpLoopUntilIdle();
+ EXPECT_EQ(GCMClientImpl::INITIAL_DEVICE_CHECKIN, gcm_client_state());
+}
+
+TEST_F(GCMClientImplStartAndStopTest, DelayedStartAndStopImmediately) {
+ // GCMClientImpl should be in INITIALIZED state at first.
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+
+ // Delay start the GCM and then stop it immediately.
+ gcm_client()->Start(GCMClient::DELAYED_START);
+ gcm_client()->Stop();
+ PumpLoopUntilIdle();
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+}
+
+TEST_F(GCMClientImplStartAndStopTest, ImmediateStartAndStopImmediately) {
+ // GCMClientImpl should be in INITIALIZED state at first.
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+
+ // Start the GCM and then stop it immediately.
+ gcm_client()->Start(GCMClient::IMMEDIATE_START);
+ gcm_client()->Stop();
+ PumpLoopUntilIdle();
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+}
+
+TEST_F(GCMClientImplStartAndStopTest, DelayedStartStopAndRestart) {
+ // GCMClientImpl should be in INITIALIZED state at first.
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+
+ // Delay start the GCM and then stop and restart it immediately.
+ gcm_client()->Start(GCMClient::DELAYED_START);
+ gcm_client()->Stop();
+ gcm_client()->Start(GCMClient::DELAYED_START);
+ PumpLoopUntilIdle();
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+}
+
+TEST_F(GCMClientImplStartAndStopTest, ImmediateStartStopAndRestart) {
+ // GCMClientImpl should be in INITIALIZED state at first.
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+
+ // Start the GCM and then stop and restart it immediately.
+ gcm_client()->Start(GCMClient::IMMEDIATE_START);
+ gcm_client()->Stop();
+ gcm_client()->Start(GCMClient::IMMEDIATE_START);
+ PumpLoopUntilIdle();
+ EXPECT_EQ(GCMClientImpl::INITIAL_DEVICE_CHECKIN, gcm_client_state());
+}
+
+TEST_F(GCMClientImplStartAndStopTest, ImmediateStartAndThenImmediateStart) {
+ // GCMClientImpl should be in INITIALIZED state at first.
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+
+ // Start the GCM immediately and complete the checkin.
+ gcm_client()->Start(GCMClient::IMMEDIATE_START);
+ PumpLoopUntilIdle();
+ EXPECT_EQ(GCMClientImpl::INITIAL_DEVICE_CHECKIN, gcm_client_state());
+ ASSERT_NO_FATAL_FAILURE(DefaultCompleteCheckin());
+ EXPECT_EQ(GCMClientImpl::READY, gcm_client_state());
+
+ // Stop the GCM.
+ gcm_client()->Stop();
+ PumpLoopUntilIdle();
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+
+ // Start the GCM immediately. GCMClientImpl should be in READY state.
+ BuildGCMClient(base::TimeDelta());
+ InitializeGCMClient();
+ gcm_client()->Start(GCMClient::IMMEDIATE_START);
+ PumpLoopUntilIdle();
+ EXPECT_EQ(GCMClientImpl::READY, gcm_client_state());
+}
+
+TEST_F(GCMClientImplStartAndStopTest, ImmediateStartAndThenDelayStart) {
+ // GCMClientImpl should be in INITIALIZED state at first.
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+
+ // Start the GCM immediately and complete the checkin.
+ gcm_client()->Start(GCMClient::IMMEDIATE_START);
+ PumpLoopUntilIdle();
+ EXPECT_EQ(GCMClientImpl::INITIAL_DEVICE_CHECKIN, gcm_client_state());
+ ASSERT_NO_FATAL_FAILURE(DefaultCompleteCheckin());
+ EXPECT_EQ(GCMClientImpl::READY, gcm_client_state());
+
+ // Stop the GCM.
+ gcm_client()->Stop();
+ PumpLoopUntilIdle();
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+
+ // Delay start the GCM. GCMClientImpl should be in LOADED state.
+ BuildGCMClient(base::TimeDelta());
+ InitializeGCMClient();
+ gcm_client()->Start(GCMClient::DELAYED_START);
+ PumpLoopUntilIdle();
+ EXPECT_EQ(GCMClientImpl::LOADED, gcm_client_state());
+}
+
+TEST_F(GCMClientImplStartAndStopTest, DISABLED_DelayedStartRace) {
+ // GCMClientImpl should be in INITIALIZED state at first.
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+
+ // Delay start the GCM, then start it immediately while it's still loading.
+ gcm_client()->Start(GCMClient::DELAYED_START);
+ gcm_client()->Start(GCMClient::IMMEDIATE_START);
+ PumpLoopUntilIdle();
+ EXPECT_EQ(GCMClientImpl::INITIAL_DEVICE_CHECKIN, gcm_client_state());
+ ASSERT_NO_FATAL_FAILURE(DefaultCompleteCheckin());
+ EXPECT_EQ(GCMClientImpl::READY, gcm_client_state());
+}
+
+TEST_F(GCMClientImplStartAndStopTest, DelayedStart) {
+ // GCMClientImpl should be in INITIALIZED state at first.
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+
+ // Delay start the GCM. The store will not be loaded and GCMClientImpl should
+ // still be in INITIALIZED state.
+ gcm_client()->Start(GCMClient::DELAYED_START);
+ PumpLoopUntilIdle();
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+
+ // Start the GCM immediately and complete the checkin.
+ gcm_client()->Start(GCMClient::IMMEDIATE_START);
+ PumpLoopUntilIdle();
+ EXPECT_EQ(GCMClientImpl::INITIAL_DEVICE_CHECKIN, gcm_client_state());
+ ASSERT_NO_FATAL_FAILURE(DefaultCompleteCheckin());
+ EXPECT_EQ(GCMClientImpl::READY, gcm_client_state());
+
+ // Registration.
+ std::vector<std::string> senders;
+ senders.push_back("sender");
+ Register(kExtensionAppId, senders);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("reg_id"));
+ EXPECT_EQ(GCMClientImpl::READY, gcm_client_state());
+
+ // Stop the GCM.
+ gcm_client()->Stop();
+ PumpLoopUntilIdle();
+ EXPECT_EQ(GCMClientImpl::INITIALIZED, gcm_client_state());
+
+ // Delay start the GCM. GCM is indeed started without delay because the
+ // registration record has been found.
+ BuildGCMClient(base::TimeDelta());
+ InitializeGCMClient();
+ gcm_client()->Start(GCMClient::DELAYED_START);
+ PumpLoopUntilIdle();
+ EXPECT_EQ(GCMClientImpl::READY, gcm_client_state());
+}
+
+// Test for known account mappings and last token fetching time being passed
+// to OnGCMReady.
+TEST_F(GCMClientImplStartAndStopTest, OnGCMReadyAccountsAndTokenFetchingTime) {
+ // Start the GCM and wait until it is ready.
+ gcm_client()->Start(GCMClient::IMMEDIATE_START);
+ PumpLoopUntilIdle();
+ ASSERT_NO_FATAL_FAILURE(DefaultCompleteCheckin());
+
+ base::Time expected_time = base::Time::Now();
+ gcm_client()->SetLastTokenFetchTime(expected_time);
+ AccountMapping expected_mapping;
+ expected_mapping.account_id = CoreAccountId("accId");
+ expected_mapping.email = "email@gmail.com";
+ expected_mapping.status = AccountMapping::MAPPED;
+ expected_mapping.status_change_timestamp = expected_time;
+ gcm_client()->UpdateAccountMapping(expected_mapping);
+ PumpLoopUntilIdle();
+
+ // Stop the GCM.
+ gcm_client()->Stop();
+ PumpLoopUntilIdle();
+
+ // Restart the GCM.
+ gcm_client()->Start(GCMClient::IMMEDIATE_START);
+ PumpLoopUntilIdle();
+
+ EXPECT_EQ(LOADING_COMPLETED, last_event());
+ EXPECT_EQ(expected_time, last_token_fetch_time());
+ ASSERT_EQ(1UL, last_account_mappings().size());
+ const AccountMapping& actual_mapping = last_account_mappings()[0];
+ EXPECT_EQ(expected_mapping.account_id, actual_mapping.account_id);
+ EXPECT_EQ(expected_mapping.email, actual_mapping.email);
+ EXPECT_EQ(expected_mapping.status, actual_mapping.status);
+ EXPECT_EQ(expected_mapping.status_change_timestamp,
+ actual_mapping.status_change_timestamp);
+}
+
+
+class GCMClientInstanceIDTest : public GCMClientImplTest {
+ public:
+ GCMClientInstanceIDTest();
+ ~GCMClientInstanceIDTest() override;
+
+ void AddInstanceID(const std::string& app_id,
+ const std::string& instance_id);
+ void RemoveInstanceID(const std::string& app_id);
+ void GetToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope);
+ void DeleteToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope);
+ void CompleteDeleteToken();
+ bool ExistsToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope) const;
+};
+
+GCMClientInstanceIDTest::GCMClientInstanceIDTest() {
+}
+
+GCMClientInstanceIDTest::~GCMClientInstanceIDTest() {
+}
+
+void GCMClientInstanceIDTest::AddInstanceID(const std::string& app_id,
+ const std::string& instance_id) {
+ gcm_client()->AddInstanceIDData(app_id, instance_id, "123");
+}
+
+void GCMClientInstanceIDTest::RemoveInstanceID(const std::string& app_id) {
+ gcm_client()->RemoveInstanceIDData(app_id);
+}
+
+void GCMClientInstanceIDTest::GetToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope) {
+ auto instance_id_info = base::MakeRefCounted<InstanceIDTokenInfo>();
+ instance_id_info->app_id = app_id;
+ instance_id_info->authorized_entity = authorized_entity;
+ instance_id_info->scope = scope;
+ gcm_client()->Register(std::move(instance_id_info));
+}
+
+void GCMClientInstanceIDTest::DeleteToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope) {
+ auto instance_id_info = base::MakeRefCounted<InstanceIDTokenInfo>();
+ instance_id_info->app_id = app_id;
+ instance_id_info->authorized_entity = authorized_entity;
+ instance_id_info->scope = scope;
+ gcm_client()->Unregister(std::move(instance_id_info));
+}
+
+void GCMClientInstanceIDTest::CompleteDeleteToken() {
+ std::string response(kDeleteTokenResponse);
+
+ EXPECT_TRUE(url_loader_factory()->SimulateResponseForPendingRequest(
+ GURL(kRegisterUrl), network::URLLoaderCompletionStatus(net::OK),
+ network::CreateURLResponseHead(net::HTTP_OK), response));
+
+ // Give a chance for GCMStoreImpl::Backend to finish persisting data.
+ PumpLoopUntilIdle();
+}
+
+bool GCMClientInstanceIDTest::ExistsToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope) const {
+ auto instance_id_info = base::MakeRefCounted<InstanceIDTokenInfo>();
+ instance_id_info->app_id = app_id;
+ instance_id_info->authorized_entity = authorized_entity;
+ instance_id_info->scope = scope;
+ return gcm_client()->registrations_.count(std::move(instance_id_info)) > 0;
+}
+
+TEST_F(GCMClientInstanceIDTest, GetToken) {
+ AddInstanceID(kExtensionAppId, kInstanceID);
+
+ // Get a token.
+ EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender, kScope));
+ GetToken(kExtensionAppId, kSender, kScope);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token1"));
+
+ EXPECT_EQ(REGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ("token1", last_registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender, kScope));
+
+ // Get another token.
+ EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender2, kScope));
+ GetToken(kExtensionAppId, kSender2, kScope);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token2"));
+
+ EXPECT_EQ(REGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ("token2", last_registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender2, kScope));
+ // The 1st token still exists.
+ EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender, kScope));
+}
+
+// Most tests in this file use kExtensionAppId which is special-cased by
+// InstanceIDUsesSubtypeForAppId in gcm_client_impl.cc. This test uses
+// kSubtypeAppId to cover the alternate case.
+TEST_F(GCMClientInstanceIDTest, GetTokenWithSubtype) {
+ ASSERT_EQ(GCMClientImpl::READY, gcm_client_state());
+
+ AddInstanceID(kSubtypeAppId, kInstanceID);
+
+ EXPECT_FALSE(ExistsToken(kSubtypeAppId, kSender, kScope));
+
+ // Get a token.
+ GetToken(kSubtypeAppId, kSender, kScope);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token1"));
+ EXPECT_EQ(REGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kSubtypeAppId, last_app_id());
+ EXPECT_EQ("token1", last_registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ EXPECT_TRUE(ExistsToken(kSubtypeAppId, kSender, kScope));
+
+ // Delete the token.
+ DeleteToken(kSubtypeAppId, kSender, kScope);
+ ASSERT_NO_FATAL_FAILURE(CompleteDeleteToken());
+ EXPECT_FALSE(ExistsToken(kSubtypeAppId, kSender, kScope));
+}
+
+TEST_F(GCMClientInstanceIDTest, DeleteInvalidToken) {
+ AddInstanceID(kExtensionAppId, kInstanceID);
+
+ // Delete an invalid token.
+ DeleteToken(kExtensionAppId, "Foo@#$", kScope);
+ PumpLoopUntilIdle();
+
+ EXPECT_EQ(UNREGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ(GCMClient::INVALID_PARAMETER, last_result());
+
+ reset_last_event();
+
+ // Delete a non-existing token.
+ DeleteToken(kExtensionAppId, kSender, kScope);
+ PumpLoopUntilIdle();
+
+ EXPECT_EQ(UNREGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ(GCMClient::INVALID_PARAMETER, last_result());
+}
+
+TEST_F(GCMClientInstanceIDTest, DeleteSingleToken) {
+ AddInstanceID(kExtensionAppId, kInstanceID);
+
+ // Get a token.
+ EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender, kScope));
+ GetToken(kExtensionAppId, kSender, kScope);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token1"));
+
+ EXPECT_EQ(REGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ("token1", last_registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender, kScope));
+
+ reset_last_event();
+
+ // Get another token.
+ EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender2, kScope));
+ GetToken(kExtensionAppId, kSender2, kScope);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token2"));
+
+ EXPECT_EQ(REGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ("token2", last_registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender2, kScope));
+ // The 1st token still exists.
+ EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender, kScope));
+
+ reset_last_event();
+
+ // Delete the 2nd token.
+ DeleteToken(kExtensionAppId, kSender2, kScope);
+ ASSERT_NO_FATAL_FAILURE(CompleteDeleteToken());
+
+ EXPECT_EQ(UNREGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ // The 2nd token is gone while the 1st token still exists.
+ EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender, kScope));
+ EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender2, kScope));
+
+ reset_last_event();
+
+ // Delete the 1st token.
+ DeleteToken(kExtensionAppId, kSender, kScope);
+ ASSERT_NO_FATAL_FAILURE(CompleteDeleteToken());
+
+ EXPECT_EQ(UNREGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ // Both tokens are gone now.
+ EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender, kScope));
+ EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender, kScope));
+
+ reset_last_event();
+
+ // Trying to delete the token again will get an error.
+ DeleteToken(kExtensionAppId, kSender, kScope);
+ PumpLoopUntilIdle();
+
+ EXPECT_EQ(UNREGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ(GCMClient::INVALID_PARAMETER, last_result());
+}
+
+TEST_F(GCMClientInstanceIDTest, DISABLED_DeleteAllTokens) {
+ AddInstanceID(kExtensionAppId, kInstanceID);
+
+ // Get a token.
+ EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender, kScope));
+ GetToken(kExtensionAppId, kSender, kScope);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token1"));
+
+ EXPECT_EQ(REGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ("token1", last_registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender, kScope));
+
+ reset_last_event();
+
+ // Get another token.
+ EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender2, kScope));
+ GetToken(kExtensionAppId, kSender2, kScope);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token2"));
+
+ EXPECT_EQ(REGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ("token2", last_registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender2, kScope));
+ // The 1st token still exists.
+ EXPECT_TRUE(ExistsToken(kExtensionAppId, kSender, kScope));
+
+ reset_last_event();
+
+ // Delete all tokens.
+ DeleteToken(kExtensionAppId, "*", "*");
+ ASSERT_NO_FATAL_FAILURE(CompleteDeleteToken());
+
+ EXPECT_EQ(UNREGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+ // All tokens are gone now.
+ EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender, kScope));
+ EXPECT_FALSE(ExistsToken(kExtensionAppId, kSender, kScope));
+}
+
+TEST_F(GCMClientInstanceIDTest, DeleteAllTokensBeforeGetAnyToken) {
+ AddInstanceID(kExtensionAppId, kInstanceID);
+
+ // Delete all tokens without getting a token first.
+ DeleteToken(kExtensionAppId, "*", "*");
+ // No need to call CompleteDeleteToken since unregistration request should
+ // not be triggered.
+ PumpLoopUntilIdle();
+
+ EXPECT_EQ(UNREGISTRATION_COMPLETED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ(GCMClient::SUCCESS, last_result());
+}
+
+TEST_F(GCMClientInstanceIDTest, DispatchDownstreamMessageWithoutSubtype) {
+ AddInstanceID(kExtensionAppId, kInstanceID);
+ GetToken(kExtensionAppId, kSender, kScope);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token1"));
+
+ std::map<std::string, std::string> expected_data;
+
+ MCSMessage message(BuildDownstreamMessage(
+ kSender, kExtensionAppId, std::string() /* subtype */, expected_data,
+ std::string() /* raw_data */));
+ EXPECT_TRUE(message.IsValid());
+ ReceiveMessageFromMCS(message);
+
+ EXPECT_EQ(MESSAGE_RECEIVED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ(expected_data.size(), last_message().data.size());
+ EXPECT_EQ(expected_data, last_message().data);
+ EXPECT_EQ(kSender, last_message().sender_id);
+}
+
+TEST_F(GCMClientInstanceIDTest, DispatchDownstreamMessageWithSubtype) {
+ AddInstanceID(kSubtypeAppId, kInstanceID);
+ GetToken(kSubtypeAppId, kSender, kScope);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token1"));
+
+ std::map<std::string, std::string> expected_data;
+
+ MCSMessage message(BuildDownstreamMessage(
+ kSender, kProductCategoryForSubtypes, kSubtypeAppId /* subtype */,
+ expected_data, std::string() /* raw_data */));
+ EXPECT_TRUE(message.IsValid());
+ ReceiveMessageFromMCS(message);
+
+ EXPECT_EQ(MESSAGE_RECEIVED, last_event());
+ EXPECT_EQ(kSubtypeAppId, last_app_id());
+ EXPECT_EQ(expected_data.size(), last_message().data.size());
+ EXPECT_EQ(expected_data, last_message().data);
+ EXPECT_EQ(kSender, last_message().sender_id);
+}
+
+TEST_F(GCMClientInstanceIDTest, DispatchDownstreamMessageWithFakeSubtype) {
+ // Victim non-extension registration.
+ AddInstanceID(kSubtypeAppId, "iid_1");
+ GetToken(kSubtypeAppId, kSender, kScope);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token1"));
+
+ // Malicious extension registration.
+ AddInstanceID(kExtensionAppId, "iid_2");
+ GetToken(kExtensionAppId, kSender, kScope);
+ ASSERT_NO_FATAL_FAILURE(CompleteRegistration("token2"));
+
+ std::map<std::string, std::string> expected_data;
+
+ // Message for kExtensionAppId should be delivered to the extension rather
+ // than the victim app, despite the malicious subtype property attempting to
+ // impersonate victim app.
+ MCSMessage message(BuildDownstreamMessage(
+ kSender, kExtensionAppId /* category */, kSubtypeAppId /* subtype */,
+ expected_data, std::string() /* raw_data */));
+ EXPECT_TRUE(message.IsValid());
+ ReceiveMessageFromMCS(message);
+
+ EXPECT_EQ(MESSAGE_RECEIVED, last_event());
+ EXPECT_EQ(kExtensionAppId, last_app_id());
+ EXPECT_EQ(expected_data.size(), last_message().data.size());
+ EXPECT_EQ(expected_data, last_message().data);
+ EXPECT_EQ(kSender, last_message().sender_id);
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_connection_observer.cc b/chromium/components/gcm_driver/gcm_connection_observer.cc
new file mode 100644
index 00000000000..5cea0b46b2a
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_connection_observer.cc
@@ -0,0 +1,18 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/gcm_connection_observer.h"
+
+namespace gcm {
+
+GCMConnectionObserver::GCMConnectionObserver() {}
+GCMConnectionObserver::~GCMConnectionObserver() {}
+
+void GCMConnectionObserver::OnConnected(const net::IPEndPoint& ip_endpoint) {
+}
+
+void GCMConnectionObserver::OnDisconnected() {
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_connection_observer.h b/chromium/components/gcm_driver/gcm_connection_observer.h
new file mode 100644
index 00000000000..15a192efcdc
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_connection_observer.h
@@ -0,0 +1,34 @@
+// Copyright (c) 2014 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 COMPONENTS_GCM_DRIVER_GCM_CONNECTION_OBSERVER_H_
+#define COMPONENTS_GCM_DRIVER_GCM_CONNECTION_OBSERVER_H_
+
+
+namespace net {
+class IPEndPoint;
+}
+
+namespace gcm {
+
+// Interface for objects observing GCM connection events.
+class GCMConnectionObserver {
+ public:
+ GCMConnectionObserver();
+ virtual ~GCMConnectionObserver();
+
+ // Called when a new connection is established and a successful handshake
+ // has been performed. Note that |ip_endpoint| is only set if available for
+ // the current platform.
+ // Default implementation does nothing.
+ virtual void OnConnected(const net::IPEndPoint& ip_endpoint);
+
+ // Called when the connection is interrupted.
+ // Default implementation does nothing.
+ virtual void OnDisconnected();
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_CONNECTION_OBSERVER_H_
diff --git a/chromium/components/gcm_driver/gcm_delayed_task_controller.cc b/chromium/components/gcm_driver/gcm_delayed_task_controller.cc
new file mode 100644
index 00000000000..15006cf52c4
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_delayed_task_controller.cc
@@ -0,0 +1,39 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/gcm_delayed_task_controller.h"
+
+#include <stddef.h>
+
+#include "base/check.h"
+
+namespace gcm {
+
+GCMDelayedTaskController::GCMDelayedTaskController() : ready_(false) {
+}
+
+GCMDelayedTaskController::~GCMDelayedTaskController() = default;
+
+void GCMDelayedTaskController::AddTask(base::OnceClosure task) {
+ delayed_tasks_.push_back(std::move(task));
+}
+
+void GCMDelayedTaskController::SetReady() {
+ ready_ = true;
+ RunTasks();
+}
+
+bool GCMDelayedTaskController::CanRunTaskWithoutDelay() const {
+ return ready_;
+}
+
+void GCMDelayedTaskController::RunTasks() {
+ DCHECK(ready_);
+
+ for (size_t i = 0; i < delayed_tasks_.size(); ++i)
+ std::move(delayed_tasks_[i]).Run();
+ delayed_tasks_.clear();
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_delayed_task_controller.h b/chromium/components/gcm_driver/gcm_delayed_task_controller.h
new file mode 100644
index 00000000000..bf1e15fa86e
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_delayed_task_controller.h
@@ -0,0 +1,44 @@
+// Copyright 2014 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 COMPONENTS_GCM_DRIVER_GCM_DELAYED_TASK_CONTROLLER_H_
+#define COMPONENTS_GCM_DRIVER_GCM_DELAYED_TASK_CONTROLLER_H_
+
+#include <vector>
+
+#include "base/callback.h"
+
+namespace gcm {
+
+// Helper class to save tasks to run until we're ready to execute them.
+class GCMDelayedTaskController {
+ public:
+ GCMDelayedTaskController();
+
+ GCMDelayedTaskController(const GCMDelayedTaskController&) = delete;
+ GCMDelayedTaskController& operator=(const GCMDelayedTaskController&) = delete;
+
+ ~GCMDelayedTaskController();
+
+ // Adds a task that will be invoked once we're ready.
+ void AddTask(base::OnceClosure task);
+
+ // Sets ready status, which will release all of the pending tasks.
+ void SetReady();
+
+ // Returns true if it is ready to perform tasks.
+ bool CanRunTaskWithoutDelay() const;
+
+ private:
+ void RunTasks();
+
+ // Flag that indicates that controlled component is ready.
+ bool ready_;
+
+ std::vector<base::OnceClosure> delayed_tasks_;
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_DELAYED_TASK_CONTROLLER_H_
diff --git a/chromium/components/gcm_driver/gcm_delayed_task_controller_unittest.cc b/chromium/components/gcm_driver/gcm_delayed_task_controller_unittest.cc
new file mode 100644
index 00000000000..e311d96eecc
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_delayed_task_controller_unittest.cc
@@ -0,0 +1,63 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/gcm_delayed_task_controller.h"
+
+#include <memory>
+
+#include "base/bind.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+class GCMDelayedTaskControllerTest : public testing::Test {
+ public:
+ GCMDelayedTaskControllerTest();
+ ~GCMDelayedTaskControllerTest() override;
+
+ void TestTask();
+
+ GCMDelayedTaskController* controller() { return controller_.get(); }
+
+ int number_of_triggered_tasks() const { return number_of_triggered_tasks_; }
+
+ private:
+ std::unique_ptr<GCMDelayedTaskController> controller_;
+ int number_of_triggered_tasks_;
+};
+
+GCMDelayedTaskControllerTest::GCMDelayedTaskControllerTest()
+ : controller_(new GCMDelayedTaskController), number_of_triggered_tasks_(0) {
+}
+
+GCMDelayedTaskControllerTest::~GCMDelayedTaskControllerTest() {
+}
+
+void GCMDelayedTaskControllerTest::TestTask() {
+ ++number_of_triggered_tasks_;
+}
+
+// Tests that a newly created controller forced tasks to be delayed, while
+// calling SetReady allows tasks to execute.
+TEST_F(GCMDelayedTaskControllerTest, SetReadyWithNoTasks) {
+ EXPECT_FALSE(controller()->CanRunTaskWithoutDelay());
+ EXPECT_EQ(0, number_of_triggered_tasks());
+
+ controller()->SetReady();
+ EXPECT_TRUE(controller()->CanRunTaskWithoutDelay());
+ EXPECT_EQ(0, number_of_triggered_tasks());
+}
+
+// Tests that tasks are triggered when controlles is set to ready.
+TEST_F(GCMDelayedTaskControllerTest, PendingTasksTriggeredWhenSetReady) {
+ controller()->AddTask(base::BindOnce(&GCMDelayedTaskControllerTest::TestTask,
+ base::Unretained(this)));
+ controller()->AddTask(base::BindOnce(&GCMDelayedTaskControllerTest::TestTask,
+ base::Unretained(this)));
+
+ controller()->SetReady();
+ EXPECT_EQ(2, number_of_triggered_tasks());
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_desktop_utils.cc b/chromium/components/gcm_driver/gcm_desktop_utils.cc
new file mode 100644
index 00000000000..b24d10ec526
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_desktop_utils.cc
@@ -0,0 +1,101 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/gcm_desktop_utils.h"
+
+#include <utility>
+
+#include "base/command_line.h"
+#include "base/task/sequenced_task_runner.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "components/gcm_driver/gcm_client_factory.h"
+#include "components/gcm_driver/gcm_driver.h"
+#include "components/gcm_driver/gcm_driver_desktop.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+#include "url/gurl.h"
+
+namespace gcm {
+
+namespace {
+
+GCMClient::ChromePlatform GetPlatform() {
+#if defined(OS_WIN)
+ return GCMClient::PLATFORM_WIN;
+#elif defined(OS_APPLE)
+ return GCMClient::PLATFORM_MAC;
+#elif defined(OS_IOS)
+ return GCMClient::PLATFORM_IOS;
+#elif defined(OS_ANDROID)
+ return GCMClient::PLATFORM_ANDROID;
+#elif BUILDFLAG(IS_CHROMEOS_ASH)
+ return GCMClient::PLATFORM_CROS;
+#elif defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS)
+ return GCMClient::PLATFORM_LINUX;
+#else
+ // For all other platforms, return as LINUX.
+ return GCMClient::PLATFORM_LINUX;
+#endif
+}
+
+GCMClient::ChromeChannel GetChannel(version_info::Channel channel) {
+ switch (channel) {
+ case version_info::Channel::UNKNOWN:
+ return GCMClient::CHANNEL_UNKNOWN;
+ case version_info::Channel::CANARY:
+ return GCMClient::CHANNEL_CANARY;
+ case version_info::Channel::DEV:
+ return GCMClient::CHANNEL_DEV;
+ case version_info::Channel::BETA:
+ return GCMClient::CHANNEL_BETA;
+ case version_info::Channel::STABLE:
+ return GCMClient::CHANNEL_STABLE;
+ }
+ NOTREACHED();
+ return GCMClient::CHANNEL_UNKNOWN;
+}
+
+std::string GetVersion() {
+ return version_info::GetVersionNumber();
+}
+
+GCMClient::ChromeBuildInfo GetChromeBuildInfo(
+ version_info::Channel channel,
+ const std::string& product_category_for_subtypes) {
+ GCMClient::ChromeBuildInfo chrome_build_info;
+ chrome_build_info.platform = GetPlatform();
+ chrome_build_info.channel = GetChannel(channel);
+ chrome_build_info.version = GetVersion();
+ chrome_build_info.product_category_for_subtypes =
+ product_category_for_subtypes;
+ return chrome_build_info;
+}
+
+} // namespace
+
+std::unique_ptr<GCMDriver> CreateGCMDriverDesktop(
+ std::unique_ptr<GCMClientFactory> gcm_client_factory,
+ PrefService* prefs,
+ const base::FilePath& store_path,
+ bool remove_account_mappings_with_email_key,
+ base::RepeatingCallback<void(
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)>
+ get_socket_factory_callback,
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
+ network::NetworkConnectionTracker* network_connection_tracker,
+ version_info::Channel channel,
+ const std::string& product_category_for_subtypes,
+ const scoped_refptr<base::SequencedTaskRunner>& ui_task_runner,
+ const scoped_refptr<base::SequencedTaskRunner>& io_task_runner,
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner) {
+ return std::unique_ptr<GCMDriver>(new GCMDriverDesktop(
+ std::move(gcm_client_factory),
+ GetChromeBuildInfo(channel, product_category_for_subtypes), prefs,
+ store_path, remove_account_mappings_with_email_key,
+ get_socket_factory_callback, std::move(url_loader_factory),
+ network_connection_tracker, ui_task_runner, io_task_runner,
+ blocking_task_runner));
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_desktop_utils.h b/chromium/components/gcm_driver/gcm_desktop_utils.h
new file mode 100644
index 00000000000..a2ed4780c53
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_desktop_utils.h
@@ -0,0 +1,49 @@
+// Copyright 2014 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 COMPONENTS_GCM_DRIVER_GCM_DESKTOP_UTILS_H_
+#define COMPONENTS_GCM_DRIVER_GCM_DESKTOP_UTILS_H_
+
+#include <memory>
+
+#include "base/memory/ref_counted.h"
+#include "base/task/sequenced_task_runner.h"
+#include "components/version_info/version_info.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "services/network/public/mojom/proxy_resolving_socket.mojom-forward.h"
+
+class PrefService;
+namespace base {
+class FilePath;
+}
+
+namespace network {
+class NetworkConnectionTracker;
+class SharedURLLoaderFactory;
+}
+
+namespace gcm {
+
+class GCMDriver;
+class GCMClientFactory;
+
+std::unique_ptr<GCMDriver> CreateGCMDriverDesktop(
+ std::unique_ptr<GCMClientFactory> gcm_client_factory,
+ PrefService* prefs,
+ const base::FilePath& store_path,
+ bool remove_account_mappings_with_email_key,
+ base::RepeatingCallback<void(
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)>
+ get_socket_factory_callback,
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
+ network::NetworkConnectionTracker* network_connection_tracker,
+ version_info::Channel channel,
+ const std::string& product_category_for_subtypes,
+ const scoped_refptr<base::SequencedTaskRunner>& ui_task_runner,
+ const scoped_refptr<base::SequencedTaskRunner>& io_task_runner,
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner);
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_DESKTOP_UTILS_H_
diff --git a/chromium/components/gcm_driver/gcm_driver.cc b/chromium/components/gcm_driver/gcm_driver.cc
new file mode 100644
index 00000000000..35960120e38
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_driver.cc
@@ -0,0 +1,373 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/gcm_driver.h"
+
+#include <stddef.h>
+
+#include <algorithm>
+
+#include "base/bind.h"
+#include "base/files/file_path.h"
+#include "base/logging.h"
+#include "base/metrics/histogram_macros.h"
+#include "components/gcm_driver/crypto/gcm_decryption_result.h"
+#include "components/gcm_driver/crypto/gcm_encryption_result.h"
+#include "components/gcm_driver/gcm_app_handler.h"
+
+namespace gcm {
+
+InstanceIDHandler::InstanceIDHandler() = default;
+
+InstanceIDHandler::~InstanceIDHandler() = default;
+
+void InstanceIDHandler::DeleteAllTokensForApp(const std::string& app_id,
+ DeleteTokenCallback callback) {
+ DeleteToken(app_id, "*", "*", std::move(callback));
+}
+
+GCMDriver::GCMDriver(
+ const base::FilePath& store_path,
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner) {
+ // The |blocking_task_runner| can be nullptr for tests that do not need the
+ // encryption capabilities of the GCMDriver class.
+ if (blocking_task_runner)
+ encryption_provider_.Init(store_path, blocking_task_runner);
+}
+
+GCMDriver::~GCMDriver() = default;
+
+void GCMDriver::Register(const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ RegisterCallback callback) {
+ DCHECK(!app_id.empty());
+ DCHECK(!sender_ids.empty() && sender_ids.size() <= kMaxSenders);
+ DCHECK(!callback.is_null());
+
+ GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START);
+ if (result != GCMClient::SUCCESS) {
+ std::move(callback).Run(std::string(), result);
+ return;
+ }
+
+ // If previous register operation is still in progress, bail out.
+ if (register_callbacks_.find(app_id) != register_callbacks_.end()) {
+ std::move(callback).Run(std::string(), GCMClient::ASYNC_OPERATION_PENDING);
+ return;
+ }
+
+ // Normalize the sender IDs by making them sorted.
+ std::vector<std::string> normalized_sender_ids = sender_ids;
+ std::sort(normalized_sender_ids.begin(), normalized_sender_ids.end());
+
+ register_callbacks_[app_id] = std::move(callback);
+
+ // If previous unregister operation is still in progress, wait until it
+ // finishes. We don't want to throw ASYNC_OPERATION_PENDING when the user
+ // uninstalls an app (ungistering) and then reinstalls the app again
+ // (registering).
+ auto unregister_iter = unregister_callbacks_.find(app_id);
+ if (unregister_iter != unregister_callbacks_.end()) {
+ // Replace the original unregister callback with an intermediate callback
+ // that will invoke the original unregister callback and trigger the pending
+ // registration after the unregistration finishes.
+ // Note that some parameters to RegisterAfterUnregister are specified here
+ // when the callback is created (base::Bind supports the partial binding
+ // of parameters).
+ unregister_iter->second = base::BindOnce(
+ &GCMDriver::RegisterAfterUnregister, weak_ptr_factory_.GetWeakPtr(),
+ app_id, normalized_sender_ids, std::move(unregister_iter->second));
+ return;
+ }
+
+ RegisterImpl(app_id, normalized_sender_ids);
+}
+
+void GCMDriver::Unregister(const std::string& app_id,
+ UnregisterCallback callback) {
+ UnregisterInternal(app_id, nullptr /* sender_id */, std::move(callback));
+}
+
+void GCMDriver::UnregisterWithSenderId(const std::string& app_id,
+ const std::string& sender_id,
+ UnregisterCallback callback) {
+ DCHECK(!sender_id.empty());
+ UnregisterInternal(app_id, &sender_id, std::move(callback));
+}
+
+void GCMDriver::UnregisterInternal(const std::string& app_id,
+ const std::string* sender_id,
+ UnregisterCallback callback) {
+ DCHECK(!app_id.empty());
+ DCHECK(!callback.is_null());
+
+ GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START);
+ if (result != GCMClient::SUCCESS) {
+ std::move(callback).Run(result);
+ return;
+ }
+
+ // If previous un/register operation is still in progress, bail out.
+ if (register_callbacks_.find(app_id) != register_callbacks_.end() ||
+ unregister_callbacks_.find(app_id) != unregister_callbacks_.end()) {
+ std::move(callback).Run(GCMClient::ASYNC_OPERATION_PENDING);
+ return;
+ }
+
+ unregister_callbacks_[app_id] = std::move(callback);
+
+ if (sender_id)
+ UnregisterWithSenderIdImpl(app_id, *sender_id);
+ else
+ UnregisterImpl(app_id);
+}
+
+void GCMDriver::Send(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message,
+ SendCallback callback) {
+ DCHECK(!app_id.empty());
+ DCHECK(!receiver_id.empty());
+ DCHECK(!callback.is_null());
+
+ GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START);
+ if (result != GCMClient::SUCCESS) {
+ std::move(callback).Run(std::string(), result);
+ return;
+ }
+
+ // If the message with send ID is still in progress, bail out.
+ std::pair<std::string, std::string> key(app_id, message.id);
+ if (send_callbacks_.find(key) != send_callbacks_.end()) {
+ std::move(callback).Run(message.id, GCMClient::INVALID_PARAMETER);
+ return;
+ }
+
+ send_callbacks_[key] = std::move(callback);
+
+ SendImpl(app_id, receiver_id, message);
+}
+
+void GCMDriver::GetEncryptionInfo(const std::string& app_id,
+ GetEncryptionInfoCallback callback) {
+ encryption_provider_.GetEncryptionInfo(app_id, "" /* authorized_entity */,
+ std::move(callback));
+}
+
+void GCMDriver::UnregisterWithSenderIdImpl(const std::string& app_id,
+ const std::string& sender_id) {
+ NOTREACHED();
+}
+
+void GCMDriver::RegisterFinished(const std::string& app_id,
+ const std::string& registration_id,
+ GCMClient::Result result) {
+ auto callback_iter = register_callbacks_.find(app_id);
+ if (callback_iter == register_callbacks_.end()) {
+ // The callback could have been removed when the app is uninstalled.
+ return;
+ }
+
+ RegisterCallback callback = std::move(callback_iter->second);
+ register_callbacks_.erase(callback_iter);
+ std::move(callback).Run(registration_id, result);
+}
+
+void GCMDriver::RemoveEncryptionInfoAfterUnregister(const std::string& app_id,
+ GCMClient::Result result) {
+ encryption_provider_.RemoveEncryptionInfo(
+ app_id, "" /* authorized_entity */,
+ base::BindOnce(&GCMDriver::UnregisterFinished,
+ weak_ptr_factory_.GetWeakPtr(), app_id, result));
+}
+
+void GCMDriver::UnregisterFinished(const std::string& app_id,
+ GCMClient::Result result) {
+ auto callback_iter = unregister_callbacks_.find(app_id);
+ if (callback_iter == unregister_callbacks_.end())
+ return;
+
+ UnregisterCallback callback = std::move(callback_iter->second);
+ unregister_callbacks_.erase(callback_iter);
+ std::move(callback).Run(result);
+}
+
+void GCMDriver::SendFinished(const std::string& app_id,
+ const std::string& message_id,
+ GCMClient::Result result) {
+ auto callback_iter = send_callbacks_.find(
+ std::pair<std::string, std::string>(app_id, message_id));
+ if (callback_iter == send_callbacks_.end()) {
+ // The callback could have been removed when the app is uninstalled.
+ return;
+ }
+
+ SendCallback callback = std::move(callback_iter->second);
+ send_callbacks_.erase(callback_iter);
+ std::move(callback).Run(message_id, result);
+}
+
+void GCMDriver::Shutdown() {
+ for (GCMAppHandlerMap::const_iterator iter = app_handlers_.begin();
+ iter != app_handlers_.end(); ++iter) {
+ DVLOG(1) << "Calling ShutdownHandler for: " << iter->first;
+ iter->second->ShutdownHandler();
+ }
+ app_handlers_.clear();
+}
+
+void GCMDriver::AddAppHandler(const std::string& app_id,
+ GCMAppHandler* handler) {
+ DCHECK(!app_id.empty());
+ DCHECK(handler);
+ DCHECK_EQ(app_handlers_.count(app_id), 0u);
+ app_handlers_[app_id] = handler;
+ DVLOG(1) << "App handler added for: " << app_id;
+}
+
+void GCMDriver::RemoveAppHandler(const std::string& app_id) {
+ DCHECK(!app_id.empty());
+ app_handlers_.erase(app_id);
+ DVLOG(1) << "App handler removed for: " << app_id;
+}
+
+GCMAppHandler* GCMDriver::GetAppHandler(const std::string& app_id) {
+ // Look for exact match.
+ GCMAppHandlerMap::const_iterator iter = app_handlers_.find(app_id);
+ if (iter != app_handlers_.end())
+ return iter->second;
+
+ // Ask the handlers whether they know how to handle it.
+ for (iter = app_handlers_.begin(); iter != app_handlers_.end(); ++iter) {
+ if (iter->second->CanHandle(app_id))
+ return iter->second;
+ }
+
+ return nullptr;
+}
+
+GCMEncryptionProvider* GCMDriver::GetEncryptionProviderInternal() {
+ return &encryption_provider_;
+}
+
+bool GCMDriver::HasRegisterCallback(const std::string& app_id) {
+ return register_callbacks_.find(app_id) != register_callbacks_.end();
+}
+
+void GCMDriver::ClearCallbacks() {
+ register_callbacks_.clear();
+ unregister_callbacks_.clear();
+ send_callbacks_.clear();
+}
+
+void GCMDriver::DispatchMessage(const std::string& app_id,
+ const IncomingMessage& message) {
+ encryption_provider_.DecryptMessage(
+ app_id, message,
+ base::BindOnce(&GCMDriver::DispatchMessageInternal,
+ weak_ptr_factory_.GetWeakPtr(), app_id));
+}
+
+void GCMDriver::DispatchMessageInternal(const std::string& app_id,
+ GCMDecryptionResult result,
+ IncomingMessage message) {
+ UMA_HISTOGRAM_ENUMERATION("GCM.Crypto.DecryptMessageResult", result,
+ GCMDecryptionResult::ENUM_SIZE);
+
+ switch (result) {
+ case GCMDecryptionResult::UNENCRYPTED:
+ case GCMDecryptionResult::DECRYPTED_DRAFT_03:
+ case GCMDecryptionResult::DECRYPTED_DRAFT_08: {
+ GCMAppHandler* handler = GetAppHandler(app_id);
+ UMA_HISTOGRAM_BOOLEAN("GCM.DeliveredToAppHandler", !!handler);
+
+ if (handler)
+ handler->OnMessage(app_id, message);
+
+ // TODO(peter/harkness): Surface unavailable app handlers on
+ // chrome://gcm-internals and send a delivery receipt.
+ return;
+ }
+ case GCMDecryptionResult::INVALID_ENCRYPTION_HEADER:
+ case GCMDecryptionResult::INVALID_CRYPTO_KEY_HEADER:
+ case GCMDecryptionResult::NO_KEYS:
+ case GCMDecryptionResult::INVALID_SHARED_SECRET:
+ case GCMDecryptionResult::INVALID_PAYLOAD:
+ case GCMDecryptionResult::INVALID_BINARY_HEADER_PAYLOAD_LENGTH:
+ case GCMDecryptionResult::INVALID_BINARY_HEADER_RECORD_SIZE:
+ case GCMDecryptionResult::INVALID_BINARY_HEADER_PUBLIC_KEY_LENGTH:
+ case GCMDecryptionResult::INVALID_BINARY_HEADER_PUBLIC_KEY_FORMAT: {
+ RecordDecryptionFailure(app_id, result);
+ GCMAppHandler* handler = GetAppHandler(app_id);
+ if (handler) {
+ handler->OnMessageDecryptionFailed(
+ app_id, message.message_id,
+ ToGCMDecryptionResultDetailsString(result));
+ }
+ return;
+ }
+ case GCMDecryptionResult::ENUM_SIZE:
+ break; // deliberate fall-through
+ }
+
+ NOTREACHED();
+}
+
+void GCMDriver::RegisterAfterUnregister(
+ const std::string& app_id,
+ const std::vector<std::string>& normalized_sender_ids,
+ UnregisterCallback unregister_callback,
+ GCMClient::Result result) {
+ // Invoke the original unregister callback.
+ std::move(unregister_callback).Run(result);
+
+ // Trigger the pending registration.
+ DCHECK(register_callbacks_.find(app_id) != register_callbacks_.end());
+ RegisterImpl(app_id, normalized_sender_ids);
+}
+
+void GCMDriver::EncryptMessage(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& p256dh,
+ const std::string& auth_secret,
+ const std::string& message,
+ EncryptMessageCallback callback) {
+ encryption_provider_.EncryptMessage(
+ app_id, authorized_entity, p256dh, auth_secret, message,
+ base::BindOnce(&GCMDriver::OnMessageEncrypted,
+ weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+}
+
+void GCMDriver::OnMessageEncrypted(EncryptMessageCallback callback,
+ GCMEncryptionResult result,
+ std::string message) {
+ UMA_HISTOGRAM_ENUMERATION("GCM.Crypto.EncryptMessageResult", result,
+ GCMEncryptionResult::ENUM_SIZE);
+ std::move(callback).Run(result, std::move(message));
+}
+
+void GCMDriver::DecryptMessage(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& message,
+ DecryptMessageCallback callback) {
+ IncomingMessage incoming_message;
+ incoming_message.sender_id = authorized_entity;
+ incoming_message.raw_data = message;
+ incoming_message.data[GCMEncryptionProvider::kContentEncodingProperty] =
+ GCMEncryptionProvider::kContentCodingAes128Gcm;
+ encryption_provider_.DecryptMessage(
+ app_id, incoming_message,
+ base::BindOnce(&GCMDriver::OnMessageDecrypted,
+ weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+}
+
+void GCMDriver::OnMessageDecrypted(DecryptMessageCallback callback,
+ GCMDecryptionResult result,
+ IncomingMessage message) {
+ UMA_HISTOGRAM_ENUMERATION("GCM.Crypto.DecryptMessageResult", result,
+ GCMDecryptionResult::ENUM_SIZE);
+ std::move(callback).Run(result, std::move(message.raw_data));
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_driver.h b/chromium/components/gcm_driver/gcm_driver.h
new file mode 100644
index 00000000000..fbbf86d2064
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_driver.h
@@ -0,0 +1,395 @@
+// Copyright 2014 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 COMPONENTS_GCM_DRIVER_GCM_DRIVER_H_
+#define COMPONENTS_GCM_DRIVER_GCM_DRIVER_H_
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include "base/callback.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "base/threading/thread_checker.h"
+#include "base/time/time.h"
+#include "components/gcm_driver/common/gcm_message.h"
+#include "components/gcm_driver/crypto/gcm_encryption_provider.h"
+#include "components/gcm_driver/gcm_client.h"
+
+namespace base {
+class FilePath;
+class SequencedTaskRunner;
+} // namespace base
+
+namespace gcm {
+
+class GCMAppHandler;
+class GCMConnectionObserver;
+enum class GCMDecryptionResult;
+enum class GCMEncryptionResult;
+struct AccountMapping;
+
+// Provides the InstanceID support via GCMDriver.
+class InstanceIDHandler {
+ public:
+ using GetTokenCallback = base::OnceCallback<void(const std::string& token,
+ GCMClient::Result result)>;
+ using ValidateTokenCallback = base::OnceCallback<void(bool is_valid)>;
+ using DeleteTokenCallback =
+ base::OnceCallback<void(GCMClient::Result result)>;
+ using GetInstanceIDDataCallback =
+ base::OnceCallback<void(const std::string& instance_id,
+ const std::string& extra_data)>;
+
+ InstanceIDHandler();
+
+ InstanceIDHandler(const InstanceIDHandler&) = delete;
+ InstanceIDHandler& operator=(const InstanceIDHandler&) = delete;
+
+ virtual ~InstanceIDHandler();
+
+ // Token service.
+ virtual void GetToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ base::TimeDelta time_to_live,
+ GetTokenCallback callback) = 0;
+ virtual void ValidateToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ const std::string& token,
+ ValidateTokenCallback callback) = 0;
+ virtual void DeleteToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ DeleteTokenCallback callback) = 0;
+ void DeleteAllTokensForApp(const std::string& app_id,
+ DeleteTokenCallback callback);
+
+ // Persistence support.
+ virtual void AddInstanceIDData(const std::string& app_id,
+ const std::string& instance_id,
+ const std::string& extra_data) = 0;
+ virtual void RemoveInstanceIDData(const std::string& app_id) = 0;
+ virtual void GetInstanceIDData(const std::string& app_id,
+ GetInstanceIDDataCallback callback) = 0;
+};
+
+// Bridge between GCM users in Chrome and the platform-specific implementation.
+// Obtain instances of this object by using |GCMProfileServiceFactory|.
+class GCMDriver {
+ public:
+ // Max number of sender IDs that can be passed to |Register| on desktop.
+ constexpr static size_t kMaxSenders = 100;
+
+ using GCMAppHandlerMap = std::map<std::string, GCMAppHandler*>;
+ using RegisterCallback =
+ base::OnceCallback<void(const std::string& registration_id,
+ GCMClient::Result result)>;
+ using ValidateRegistrationCallback = base::OnceCallback<void(bool is_valid)>;
+ using UnregisterCallback = base::OnceCallback<void(GCMClient::Result result)>;
+ using SendCallback = base::OnceCallback<void(const std::string& message_id,
+ GCMClient::Result result)>;
+ using GetEncryptionInfoCallback =
+ base::OnceCallback<void(std::string p256dh, std::string auth_secret)>;
+ using EncryptMessageCallback =
+ base::OnceCallback<void(GCMEncryptionResult result, std::string message)>;
+ using DecryptMessageCallback =
+ base::OnceCallback<void(GCMDecryptionResult result, std::string message)>;
+
+ using GetGCMStatisticsCallback =
+ base::OnceCallback<void(const GCMClient::GCMStatistics& stats)>;
+ using GCMStatisticsRecordingCallback =
+ base::RepeatingCallback<void(const GCMClient::GCMStatistics& stats)>;
+
+ // Enumeration to be used with GetGCMStatistics() for indicating whether the
+ // existing logs should be cleared or kept.
+ enum ClearActivityLogs { CLEAR_LOGS, KEEP_LOGS };
+
+ GCMDriver(
+ const base::FilePath& store_path,
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner);
+
+ GCMDriver(const GCMDriver&) = delete;
+ GCMDriver& operator=(const GCMDriver&) = delete;
+
+ virtual ~GCMDriver();
+
+ // Registers |sender_ids| for an app. *Use |InstanceID| instead in new code.*
+ //
+ // A registration ID will be returned by the GCM server. On Android, only a
+ // single sender ID is supported, but instead multiple simultaneous
+ // registrations are allowed.
+ // |app_id|: application ID.
+ // |sender_ids|: list of IDs of the servers allowed to send messages to the
+ // application. The IDs are assigned by the Google API Console.
+ // Max number of IDs is 1 on Android, |kMaxSenders| on desktop.
+ // |callback|: to be called once the asynchronous operation is done.
+ void Register(const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ RegisterCallback callback);
+
+ // Checks that the provided |sender_ids| and |registration_id| matches the
+ // stored registration info for |app_id|.
+ virtual void ValidateRegistration(const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ const std::string& registration_id,
+ ValidateRegistrationCallback callback) = 0;
+
+ // Unregisters all sender_ids for an app. Only works on non-Android. Will also
+ // remove any encryption keys associated with the |app_id|.
+ // |app_id|: application ID.
+ // |callback|: to be called once the asynchronous operation is done.
+ void Unregister(const std::string& app_id, UnregisterCallback callback);
+
+ // Unregisters an (app_id, sender_id) pair from using GCM. Only works on
+ // Android. Will also remove any encryption keys associated with the |app_id|.
+ // TODO(jianli): Switch to using GCM's unsubscribe API.
+ // |app_id|: application ID.
+ // |sender_id|: the sender ID that was passed when registering.
+ // |callback|: to be called once the asynchronous operation is done.
+ void UnregisterWithSenderId(const std::string& app_id,
+ const std::string& sender_id,
+ UnregisterCallback callback);
+
+ // Sends a message to a given receiver.
+ // |app_id|: application ID.
+ // |receiver_id|: registration ID of the receiver party.
+ // |message|: message to be sent.
+ // |callback|: to be called once the asynchronous operation is done.
+ void Send(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message,
+ SendCallback callback);
+
+ // Get the public encryption key and the authentication secret associated with
+ // |app_id|. If none have been associated with |app_id| yet, they will be
+ // created. The |callback| will be invoked when it is available. Only use with
+ // GCM registrations; use InstanceID::GetEncryptionInfo for InstanceID tokens.
+ virtual void GetEncryptionInfo(const std::string& app_id,
+ GetEncryptionInfoCallback callback);
+
+ // Attempts to encrypt the |message| using draft-ietf-webpush-encryption-08
+ // scheme using keys from internal key store. Either GetEncryptionInfo or
+ // InstanceID::GetEncryptionInfo must be called once for keys to be available.
+ // |callback| will be called asynchronously when |message| has been encrypted.
+ // A dispatchable message will be used in case of success, an empty message in
+ // case of failure.
+ virtual void EncryptMessage(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& p256dh,
+ const std::string& auth_secret,
+ const std::string& message,
+ EncryptMessageCallback callback);
+
+ // Attempts to decrypt the |message|using draft-ietf-webpush-encryption-08
+ // scheme using keys from internal key store. Either GetEncryptionInfo or
+ // InstanceID::GetEncryptionInfo must be called once for keys to be available.
+ // |callback| will be called asynchronously when |message| has been decrypted.
+ // A dispatchable message will be used in case of success, an empty message in
+ // case of failure.
+ // TODO(crbug/1045907): Decouple this from GCMDriver.
+ virtual void DecryptMessage(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& message,
+ DecryptMessageCallback callback);
+
+ const GCMAppHandlerMap& app_handlers() const { return app_handlers_; }
+
+ // This method must be called before destroying the GCMDriver. Once it has
+ // been called, no other GCMDriver methods may be used.
+ virtual void Shutdown();
+
+ // Called when the user signs in to or out of a GAIA account.
+ virtual void OnSignedIn() = 0;
+ virtual void OnSignedOut() = 0;
+
+ // Adds a handler for a given app.
+ virtual void AddAppHandler(const std::string& app_id, GCMAppHandler* handler);
+
+ // Remove the handler for a given app.
+ virtual void RemoveAppHandler(const std::string& app_id);
+
+ // Returns the handler for the given app. May return a nullptr when no handler
+ // could be found for the |app_id|.
+ GCMAppHandler* GetAppHandler(const std::string& app_id);
+
+ // Adds a connection state observer.
+ virtual void AddConnectionObserver(GCMConnectionObserver* observer) = 0;
+
+ // Removes a connection state observer.
+ virtual void RemoveConnectionObserver(GCMConnectionObserver* observer) = 0;
+
+ // For testing purpose. Always NULL on Android.
+ virtual GCMClient* GetGCMClientForTesting() const = 0;
+
+ // Returns true if the service was started.
+ virtual bool IsStarted() const = 0;
+
+ // Returns true if the gcm client has an open and active connection.
+ virtual bool IsConnected() const = 0;
+
+ // Get GCM client internal states and statistics. The activity logs will be
+ // cleared before returning the stats when |clear_logs| is set to CLEAR_LOGS.
+ virtual void GetGCMStatistics(GetGCMStatisticsCallback callback,
+ ClearActivityLogs clear_logs) = 0;
+
+ // Enables/disables GCM activity recording, and then returns the stats.
+ // |callback| will be called for new activity.
+ virtual void SetGCMRecording(const GCMStatisticsRecordingCallback& callback,
+ bool recording) = 0;
+
+ // sets a list of signed in accounts with OAuth2 access tokens, when GCMDriver
+ // works in context of a signed in entity (e.g. browser profile where user is
+ // signed into sync).
+ // |account_tokens|: list of email addresses, account IDs and OAuth2 access
+ // tokens.
+ virtual void SetAccountTokens(
+ const std::vector<GCMClient::AccountTokenInfo>& account_tokens) = 0;
+
+ // Updates the |account_mapping| information in persistent store.
+ virtual void UpdateAccountMapping(const AccountMapping& account_mapping) = 0;
+
+ // Removes the account mapping information reated to |account_id| from
+ // persistent store.
+ virtual void RemoveAccountMapping(const CoreAccountId& account_id) = 0;
+
+ // Getter and setter of last token fetch time.
+ virtual base::Time GetLastTokenFetchTime() = 0;
+ virtual void SetLastTokenFetchTime(const base::Time& time) = 0;
+
+ // These methods must only be used by the InstanceID system.
+ // The InstanceIDHandler provides an implementation for the InstanceID system.
+ virtual InstanceIDHandler* GetInstanceIDHandlerInternal() = 0;
+ // Allows the InstanceID system to integrate with GCM encryption storage.
+ virtual GCMEncryptionProvider* GetEncryptionProviderInternal();
+
+ // Adds or removes a custom client requested heartbeat interval. If multiple
+ // components set that setting, the lowest setting will be used. If the
+ // setting is outside of GetMax/MinClientHeartbeatIntervalMs() it will be
+ // ignored. If a new setting is less than the currently used, the connection
+ // will be reset with the new heartbeat. Client that no longer require
+ // aggressive heartbeats, should remove their requested interval. Heartbeats
+ // set this way survive connection/Chrome restart.
+ //
+ // GCM Driver can decide to postpone the action until Client is properly
+ // initialized, hence this setting can be called at any time.
+ //
+ // Server can overwrite the setting to a different value.
+ //
+ // |scope| is used to identify the component that requests a custom interval
+ // to be set, and allows that component to later revoke the setting.
+ // |interval_ms| should be between 2 minues and 15 minues (28 minues on
+ // cellular networks). For details check
+ // GetMin/MaxClientHeartbeatItnervalMs() in HeartbeatManager.
+ virtual void AddHeartbeatInterval(const std::string& scope,
+ int interval_ms) = 0;
+ virtual void RemoveHeartbeatInterval(const std::string& scope) = 0;
+
+ protected:
+ // Ensures that the GCM service starts (if necessary conditions are met).
+ virtual GCMClient::Result EnsureStarted(GCMClient::StartMode start_mode) = 0;
+
+ // Platform-specific implementation of Register.
+ virtual void RegisterImpl(const std::string& app_id,
+ const std::vector<std::string>& sender_ids) = 0;
+
+ // Platform-specific implementation of Unregister.
+ virtual void UnregisterImpl(const std::string& app_id) = 0;
+
+ // Platform-specific implementation of UnregisterWithSenderId.
+ virtual void UnregisterWithSenderIdImpl(const std::string& app_id,
+ const std::string& sender_id);
+
+ // Platform-specific implementation of Send.
+ virtual void SendImpl(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) = 0;
+
+ // Platform-specific implementation of recording message decryption failures.
+ virtual void RecordDecryptionFailure(const std::string& app_id,
+ GCMDecryptionResult result) = 0;
+
+ // Runs the Register callback.
+ void RegisterFinished(const std::string& app_id,
+ const std::string& registration_id,
+ GCMClient::Result result);
+
+ // To be called when a registration for |app_id| has been unregistered, having
+ // |result| as the result of the unregistration. Will remove any encryption
+ // information associated with the |app_id| and then calls UnregisterFinished.
+ void RemoveEncryptionInfoAfterUnregister(const std::string& app_id,
+ GCMClient::Result result);
+
+ // Runs the Unregister callback.
+ void UnregisterFinished(const std::string& app_id, GCMClient::Result result);
+
+ // Runs the Send callback.
+ void SendFinished(const std::string& app_id,
+ const std::string& message_id,
+ GCMClient::Result result);
+
+ bool HasRegisterCallback(const std::string& app_id);
+
+ void ClearCallbacks();
+
+ // Dispatches the OnMessage event to the app handler associated with |app_id|.
+ // If |message| has been encrypted, it will be decrypted asynchronously and
+ // dispatched when the decryption operation was successful. Otherwise, the
+ // |message| will be dispatched immediately to the handler for |app_id|.
+ void DispatchMessage(const std::string& app_id,
+ const IncomingMessage& message);
+
+ private:
+ // Common code shared by Unregister and UnregisterWithSenderId.
+ void UnregisterInternal(const std::string& app_id,
+ const std::string* sender_id,
+ UnregisterCallback callback);
+
+ // Dispatches the OnMessage event to the app handler associated with |app_id|
+ // if |result| indicates that it is safe to do so, or will report a decryption
+ // failure for the |app_id| otherwise.
+ void DispatchMessageInternal(const std::string& app_id,
+ GCMDecryptionResult result,
+ IncomingMessage message);
+
+ // Called after unregistration completes in order to trigger the pending
+ // registration.
+ void RegisterAfterUnregister(
+ const std::string& app_id,
+ const std::vector<std::string>& normalized_sender_ids,
+ UnregisterCallback unregister_callback,
+ GCMClient::Result result);
+
+ void OnMessageEncrypted(EncryptMessageCallback callback,
+ GCMEncryptionResult result,
+ std::string message);
+
+ void OnMessageDecrypted(DecryptMessageCallback callback,
+ GCMDecryptionResult result,
+ IncomingMessage message);
+
+ // Callback map (from app_id to callback) for Register.
+ std::map<std::string, RegisterCallback> register_callbacks_;
+
+ // Callback map (from app_id to callback) for Unregister.
+ std::map<std::string, UnregisterCallback> unregister_callbacks_;
+
+ // Callback map (from <app_id, message_id> to callback) for Send.
+ std::map<std::pair<std::string, std::string>, SendCallback> send_callbacks_;
+
+ // The encryption provider, used for key management and decryption of
+ // encrypted, incoming messages.
+ GCMEncryptionProvider encryption_provider_;
+
+ // App handler map (from app_id to handler pointer). The handler is not owned.
+ GCMAppHandlerMap app_handlers_;
+
+ base::WeakPtrFactory<GCMDriver> weak_ptr_factory_{this};
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_DRIVER_H_
diff --git a/chromium/components/gcm_driver/gcm_driver_android.cc b/chromium/components/gcm_driver/gcm_driver_android.cc
new file mode 100644
index 00000000000..89280e122f1
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_driver_android.cc
@@ -0,0 +1,275 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/gcm_driver_android.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "base/android/jni_android.h"
+#include "base/android/jni_array.h"
+#include "base/android/jni_string.h"
+#include "base/compiler_specific.h"
+#include "base/logging.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "components/gcm_driver/android/jni_headers/GCMDriver_jni.h"
+
+using base::android::AppendJavaStringArrayToStringVector;
+using base::android::AttachCurrentThread;
+using base::android::ConvertJavaStringToUTF8;
+using base::android::ConvertUTF8ToJavaString;
+using base::android::JavaByteArrayToString;
+using base::android::JavaParamRef;
+
+namespace gcm {
+
+GCMDriverAndroid::GCMDriverAndroid(
+ const base::FilePath& store_path,
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner)
+ : GCMDriver(store_path, blocking_task_runner), recorder_(this) {
+ JNIEnv* env = AttachCurrentThread();
+ java_ref_.Reset(Java_GCMDriver_create(env, reinterpret_cast<intptr_t>(this)));
+}
+
+GCMDriverAndroid::~GCMDriverAndroid() {
+ JNIEnv* env = AttachCurrentThread();
+ Java_GCMDriver_destroy(env, java_ref_);
+}
+
+void GCMDriverAndroid::OnRegisterFinished(
+ JNIEnv* env,
+ const JavaParamRef<jobject>& obj,
+ const JavaParamRef<jstring>& j_app_id,
+ const JavaParamRef<jstring>& j_registration_id,
+ jboolean success) {
+ std::string app_id = ConvertJavaStringToUTF8(env, j_app_id);
+ std::string registration_id = ConvertJavaStringToUTF8(env, j_registration_id);
+ GCMClient::Result result =
+ success ? GCMClient::SUCCESS : GCMClient::UNKNOWN_ERROR;
+
+ recorder_.RecordRegistrationResponse(app_id, success);
+
+ RegisterFinished(app_id, registration_id, result);
+}
+
+void GCMDriverAndroid::OnUnregisterFinished(
+ JNIEnv* env,
+ const JavaParamRef<jobject>& obj,
+ const JavaParamRef<jstring>& j_app_id,
+ jboolean success) {
+ std::string app_id = ConvertJavaStringToUTF8(env, j_app_id);
+ GCMClient::Result result =
+ success ? GCMClient::SUCCESS : GCMClient::UNKNOWN_ERROR;
+
+ recorder_.RecordUnregistrationResponse(app_id, success);
+
+ RemoveEncryptionInfoAfterUnregister(app_id, result);
+}
+
+void GCMDriverAndroid::OnMessageReceived(
+ JNIEnv* env,
+ const JavaParamRef<jobject>& obj,
+ const JavaParamRef<jstring>& j_app_id,
+ const JavaParamRef<jstring>& j_sender_id,
+ const JavaParamRef<jstring>& j_message_id,
+ const JavaParamRef<jstring>& j_collapse_key,
+ const JavaParamRef<jbyteArray>& j_raw_data,
+ const JavaParamRef<jobjectArray>& j_data_keys_and_values) {
+ std::string app_id = ConvertJavaStringToUTF8(env, j_app_id);
+
+ int message_byte_size = 0;
+
+ IncomingMessage message;
+ message.sender_id = ConvertJavaStringToUTF8(env, j_sender_id);
+
+ if (!j_message_id.is_null())
+ ConvertJavaStringToUTF8(env, j_message_id, &message.message_id);
+ if (!j_collapse_key.is_null())
+ ConvertJavaStringToUTF8(env, j_collapse_key, &message.collapse_key);
+
+ // Expand j_data_keys_and_values from array to map.
+ std::vector<std::string> data_keys_and_values;
+ AppendJavaStringArrayToStringVector(env, j_data_keys_and_values,
+ &data_keys_and_values);
+ for (size_t i = 0; i + 1 < data_keys_and_values.size(); i += 2) {
+ message.data[data_keys_and_values[i]] = data_keys_and_values[i + 1];
+ message_byte_size += data_keys_and_values[i + 1].size();
+ }
+ // Convert j_raw_data from byte[] to binary std::string.
+ if (j_raw_data) {
+ JavaByteArrayToString(env, j_raw_data, &message.raw_data);
+
+ message_byte_size += message.raw_data.size();
+ }
+
+ recorder_.RecordDataMessageReceived(app_id, message.sender_id,
+ message_byte_size);
+
+ DispatchMessage(app_id, message);
+}
+
+void GCMDriverAndroid::ValidateRegistration(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ const std::string& registration_id,
+ ValidateRegistrationCallback callback) {
+ // gcm_driver doesn't store registration IDs on Android, so assume it's valid.
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::BindOnce(std::move(callback), true /* is_valid */));
+}
+
+void GCMDriverAndroid::OnSignedIn() {}
+
+void GCMDriverAndroid::OnSignedOut() {}
+
+void GCMDriverAndroid::AddAppHandler(const std::string& app_id,
+ GCMAppHandler* handler) {
+ GCMDriver::AddAppHandler(app_id, handler);
+ JNIEnv* env = AttachCurrentThread();
+ // TODO(melandory, mamir): check if messages were persisted
+ // and only then go to java.
+ Java_GCMDriver_replayPersistedMessages(env, java_ref_,
+ ConvertUTF8ToJavaString(env, app_id));
+}
+
+void GCMDriverAndroid::AddConnectionObserver(GCMConnectionObserver* observer) {}
+
+void GCMDriverAndroid::RemoveConnectionObserver(
+ GCMConnectionObserver* observer) {}
+
+GCMClient* GCMDriverAndroid::GetGCMClientForTesting() const {
+ NOTIMPLEMENTED();
+ return NULL;
+}
+
+bool GCMDriverAndroid::IsStarted() const {
+ return true;
+}
+
+bool GCMDriverAndroid::IsConnected() const {
+ // TODO(gcm): hook up to GCM connected status
+ return true;
+}
+
+void GCMDriverAndroid::GetGCMStatistics(GetGCMStatisticsCallback callback,
+ ClearActivityLogs clear_logs) {
+ DCHECK(!callback.is_null());
+
+ if (clear_logs == CLEAR_LOGS)
+ recorder_.Clear();
+
+ GCMClient::GCMStatistics stats;
+ stats.is_recording = recorder_.is_recording();
+
+ recorder_.CollectActivities(&stats.recorded_activities);
+
+ std::move(callback).Run(stats);
+}
+
+void GCMDriverAndroid::SetGCMRecording(
+ const GCMStatisticsRecordingCallback& callback,
+ bool recording) {
+ DCHECK(!callback.is_null());
+
+ gcm_statistics_recording_callback_ = callback;
+ recorder_.set_is_recording(recording);
+
+ GCMClient::GCMStatistics stats;
+ stats.is_recording = recording;
+
+ recorder_.CollectActivities(&stats.recorded_activities);
+
+ callback.Run(stats);
+}
+
+void GCMDriverAndroid::SetAccountTokens(
+ const std::vector<GCMClient::AccountTokenInfo>& account_tokens) {
+ NOTIMPLEMENTED();
+}
+
+void GCMDriverAndroid::UpdateAccountMapping(
+ const AccountMapping& account_mapping) {
+ NOTIMPLEMENTED();
+}
+
+void GCMDriverAndroid::RemoveAccountMapping(const CoreAccountId& account_id) {
+ NOTIMPLEMENTED();
+}
+
+base::Time GCMDriverAndroid::GetLastTokenFetchTime() {
+ NOTIMPLEMENTED();
+ return base::Time();
+}
+
+void GCMDriverAndroid::SetLastTokenFetchTime(const base::Time& time) {
+ NOTIMPLEMENTED();
+}
+
+InstanceIDHandler* GCMDriverAndroid::GetInstanceIDHandlerInternal() {
+ // Not supported for Android.
+ return NULL;
+}
+
+void GCMDriverAndroid::AddHeartbeatInterval(const std::string& scope,
+ int interval_ms) {}
+
+void GCMDriverAndroid::RemoveHeartbeatInterval(const std::string& scope) {}
+
+void GCMDriverAndroid::OnActivityRecorded() {
+ DCHECK(gcm_statistics_recording_callback_);
+
+ GCMClient::GCMStatistics stats;
+ stats.is_recording = recorder_.is_recording();
+
+ recorder_.CollectActivities(&stats.recorded_activities);
+
+ gcm_statistics_recording_callback_.Run(stats);
+}
+
+GCMClient::Result GCMDriverAndroid::EnsureStarted(
+ GCMClient::StartMode start_mode) {
+ // TODO(johnme): Maybe we should check if GMS is available?
+ return GCMClient::SUCCESS;
+}
+
+void GCMDriverAndroid::RegisterImpl(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids) {
+ DCHECK_EQ(1u, sender_ids.size());
+ JNIEnv* env = AttachCurrentThread();
+
+ recorder_.RecordRegistrationSent(app_id);
+
+ Java_GCMDriver_register(env, java_ref_, ConvertUTF8ToJavaString(env, app_id),
+ ConvertUTF8ToJavaString(env, sender_ids[0]));
+}
+
+void GCMDriverAndroid::UnregisterImpl(const std::string& app_id) {
+ NOTREACHED();
+}
+
+void GCMDriverAndroid::UnregisterWithSenderIdImpl(
+ const std::string& app_id,
+ const std::string& sender_id) {
+ JNIEnv* env = AttachCurrentThread();
+
+ recorder_.RecordUnregistrationSent(app_id);
+
+ Java_GCMDriver_unregister(env, java_ref_,
+ ConvertUTF8ToJavaString(env, app_id),
+ ConvertUTF8ToJavaString(env, sender_id));
+}
+
+void GCMDriverAndroid::SendImpl(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) {
+ NOTIMPLEMENTED();
+}
+
+void GCMDriverAndroid::RecordDecryptionFailure(const std::string& app_id,
+ GCMDecryptionResult result) {
+ recorder_.RecordDecryptionFailure(app_id, result);
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_driver_android.h b/chromium/components/gcm_driver/gcm_driver_android.h
new file mode 100644
index 00000000000..d4cac51ce2a
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_driver_android.h
@@ -0,0 +1,115 @@
+// Copyright 2014 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 COMPONENTS_GCM_DRIVER_GCM_DRIVER_ANDROID_H_
+#define COMPONENTS_GCM_DRIVER_GCM_DRIVER_ANDROID_H_
+
+#include <jni.h>
+
+#include "base/android/scoped_java_ref.h"
+#include "base/bind.h"
+#include "base/compiler_specific.h"
+#include "base/memory/ref_counted.h"
+#include "components/gcm_driver/gcm_driver.h"
+#include "components/gcm_driver/gcm_stats_recorder_android.h"
+
+namespace base {
+class FilePath;
+class SequencedTaskRunner;
+}
+
+namespace gcm {
+
+// GCMDriver implementation for Android, using Android GCM APIs.
+class GCMDriverAndroid : public GCMDriver,
+ public GCMStatsRecorderAndroid::Delegate {
+ public:
+ GCMDriverAndroid(
+ const base::FilePath& store_path,
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner);
+
+ GCMDriverAndroid(const GCMDriverAndroid&) = delete;
+ GCMDriverAndroid& operator=(const GCMDriverAndroid&) = delete;
+
+ ~GCMDriverAndroid() override;
+
+ // Methods called from Java via JNI:
+ void OnRegisterFinished(
+ JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj,
+ const base::android::JavaParamRef<jstring>& app_id,
+ const base::android::JavaParamRef<jstring>& registration_id,
+ jboolean success);
+ void OnUnregisterFinished(JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj,
+ const base::android::JavaParamRef<jstring>& app_id,
+ jboolean success);
+ void OnMessageReceived(
+ JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj,
+ const base::android::JavaParamRef<jstring>& app_id,
+ const base::android::JavaParamRef<jstring>& sender_id,
+ const base::android::JavaParamRef<jstring>& j_message_id,
+ const base::android::JavaParamRef<jstring>& collapse_key,
+ const base::android::JavaParamRef<jbyteArray>& raw_data,
+ const base::android::JavaParamRef<jobjectArray>& data_keys_and_values);
+
+ // GCMDriver implementation:
+ void ValidateRegistration(const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ const std::string& registration_id,
+ ValidateRegistrationCallback callback) override;
+ void OnSignedIn() override;
+ void OnSignedOut() override;
+ void AddConnectionObserver(GCMConnectionObserver* observer) override;
+ void RemoveConnectionObserver(GCMConnectionObserver* observer) override;
+ GCMClient* GetGCMClientForTesting() const override;
+ bool IsStarted() const override;
+ bool IsConnected() const override;
+ void GetGCMStatistics(GetGCMStatisticsCallback callback,
+ ClearActivityLogs clear_logs) override;
+ void SetGCMRecording(const GCMStatisticsRecordingCallback& callback,
+ bool recording) override;
+ void SetAccountTokens(
+ const std::vector<GCMClient::AccountTokenInfo>& account_tokens) override;
+ void UpdateAccountMapping(const AccountMapping& account_mapping) override;
+ void RemoveAccountMapping(const CoreAccountId& account_id) override;
+ base::Time GetLastTokenFetchTime() override;
+ void SetLastTokenFetchTime(const base::Time& time) override;
+ InstanceIDHandler* GetInstanceIDHandlerInternal() override;
+ void AddHeartbeatInterval(const std::string& scope, int interval_ms) override;
+ void RemoveHeartbeatInterval(const std::string& scope) override;
+ void AddAppHandler(const std::string& app_id,
+ GCMAppHandler* handler) override;
+
+ // GCMStatsRecorder::Delegate implementation:
+ void OnActivityRecorded() override;
+
+ protected:
+ // GCMDriver implementation:
+ GCMClient::Result EnsureStarted(GCMClient::StartMode start_mode) override;
+ void RegisterImpl(const std::string& app_id,
+ const std::vector<std::string>& sender_ids) override;
+ void UnregisterImpl(const std::string& app_id) override;
+ void UnregisterWithSenderIdImpl(const std::string& app_id,
+ const std::string& sender_id) override;
+ void SendImpl(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) override;
+ void RecordDecryptionFailure(const std::string& app_id,
+ GCMDecryptionResult result) override;
+
+ private:
+ base::android::ScopedJavaGlobalRef<jobject> java_ref_;
+
+ // Callback for SetGCMRecording.
+ GCMStatisticsRecordingCallback gcm_statistics_recording_callback_;
+
+ // Recorder that logs GCM activities.
+ GCMStatsRecorderAndroid recorder_;
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_DRIVER_ANDROID_H_
diff --git a/chromium/components/gcm_driver/gcm_driver_constants.cc b/chromium/components/gcm_driver/gcm_driver_constants.cc
new file mode 100644
index 00000000000..1b3675d501f
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_driver_constants.cc
@@ -0,0 +1,15 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/gcm_driver_constants.h"
+
+#define FPL FILE_PATH_LITERAL
+
+namespace gcm_driver {
+
+const base::FilePath::CharType kGCMStoreDirname[] = FPL("GCM Store");
+
+} // namespace gcm_driver
+
+#undef FPL
diff --git a/chromium/components/gcm_driver/gcm_driver_constants.h b/chromium/components/gcm_driver/gcm_driver_constants.h
new file mode 100644
index 00000000000..3501ead17a6
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_driver_constants.h
@@ -0,0 +1,20 @@
+// Copyright 2015 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.
+
+// A handful of resource-like constants related to the GCM(Google Cloud
+// Messaging) Driver.
+
+#ifndef COMPONENTS_GCM_DRIVER_GCM_DRIVER_CONSTANTS_H_
+#define COMPONENTS_GCM_DRIVER_GCM_DRIVER_CONSTANTS_H_
+
+#include "base/files/file_path.h"
+
+namespace gcm_driver {
+
+// File path for GCM Store.
+extern const base::FilePath::CharType kGCMStoreDirname[];
+
+} // namespace gcm_driver
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_DRIVER_CONSTANTS_H_
diff --git a/chromium/components/gcm_driver/gcm_driver_desktop.cc b/chromium/components/gcm_driver/gcm_driver_desktop.cc
new file mode 100644
index 00000000000..319422bf39b
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_driver_desktop.cc
@@ -0,0 +1,1333 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/gcm_driver_desktop.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/files/file_path.h"
+#include "base/location.h"
+#include "base/logging.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/task/task_runner_util.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/timer/timer.h"
+#include "build/build_config.h"
+#include "build/chromeos_buildflags.h"
+#include "components/gcm_driver/gcm_account_mapper.h"
+#include "components/gcm_driver/gcm_app_handler.h"
+#include "components/gcm_driver/gcm_client_factory.h"
+#include "components/gcm_driver/gcm_delayed_task_controller.h"
+#include "components/gcm_driver/instance_id/instance_id_impl.h"
+#include "components/gcm_driver/system_encryptor.h"
+#include "google_apis/gcm/engine/account_mapping.h"
+#include "net/base/ip_endpoint.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+
+namespace gcm {
+
+class GCMDriverDesktop::IOWorker : public GCMClient::Delegate {
+ public:
+ // Called on UI thread.
+ IOWorker(const scoped_refptr<base::SequencedTaskRunner>& ui_thread,
+ const scoped_refptr<base::SequencedTaskRunner>& io_thread);
+
+ IOWorker(const IOWorker&) = delete;
+ IOWorker& operator=(const IOWorker&) = delete;
+
+ virtual ~IOWorker();
+
+ // Overridden from GCMClient::Delegate:
+ // Called on IO thread.
+ void OnRegisterFinished(scoped_refptr<RegistrationInfo> registration_info,
+ const std::string& registration_id,
+ GCMClient::Result result) override;
+ void OnUnregisterFinished(scoped_refptr<RegistrationInfo> registration_info,
+ GCMClient::Result result) override;
+ void OnSendFinished(const std::string& app_id,
+ const std::string& message_id,
+ GCMClient::Result result) override;
+ void OnMessageReceived(const std::string& app_id,
+ const IncomingMessage& message) override;
+ void OnMessagesDeleted(const std::string& app_id) override;
+ void OnMessageSendError(
+ const std::string& app_id,
+ const GCMClient::SendErrorDetails& send_error_details) override;
+ void OnSendAcknowledged(const std::string& app_id,
+ const std::string& message_id) override;
+ void OnGCMReady(const std::vector<AccountMapping>& account_mappings,
+ const base::Time& last_token_fetch_time) override;
+ void OnActivityRecorded() override;
+ void OnConnected(const net::IPEndPoint& ip_endpoint) override;
+ void OnDisconnected() override;
+ void OnStoreReset() override;
+
+ // Called on IO thread.
+ void Initialize(
+ std::unique_ptr<GCMClientFactory> gcm_client_factory,
+ const GCMClient::ChromeBuildInfo& chrome_build_info,
+ const base::FilePath& store_path,
+ bool remove_account_mappings_with_email_key,
+ base::RepeatingCallback<void(
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)>
+ get_socket_factory_callback,
+ std::unique_ptr<network::PendingSharedURLLoaderFactory>
+ pending_loader_factory,
+ network::NetworkConnectionTracker* network_connection_tracker,
+ const scoped_refptr<base::SequencedTaskRunner> blocking_task_runner);
+ void Start(GCMClient::StartMode start_mode,
+ const base::WeakPtr<GCMDriverDesktop>& service);
+ void Stop();
+ void Register(const std::string& app_id,
+ const std::vector<std::string>& sender_ids);
+ void Unregister(const std::string& app_id);
+ void Send(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message);
+ void GetGCMStatistics(GetGCMStatisticsCallback callback,
+ GCMDriver::ClearActivityLogs clear_logs);
+ void SetGCMRecording(GetGCMStatisticsCallback callback, bool recording);
+
+ void SetAccountTokens(
+ const std::vector<GCMClient::AccountTokenInfo>& account_tokens);
+ void UpdateAccountMapping(const AccountMapping& account_mapping);
+ void RemoveAccountMapping(const CoreAccountId& account_id);
+ void SetLastTokenFetchTime(const base::Time& time);
+ void AddHeartbeatInterval(const std::string& scope, int interval_ms);
+ void RemoveHeartbeatInterval(const std::string& scope);
+
+ void AddInstanceIDData(const std::string& app_id,
+ const std::string& instance_id,
+ const std::string& extra_data);
+ void RemoveInstanceIDData(const std::string& app_id);
+ void GetInstanceIDData(const std::string& app_id);
+ void GetToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ base::TimeDelta time_to_live);
+ bool ValidateRegistration(scoped_refptr<RegistrationInfo> registration_info,
+ const std::string& registration_id);
+ void DeleteToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope);
+
+ void RecordDecryptionFailure(const std::string& app_id,
+ GCMDecryptionResult result);
+
+ // For testing purpose. Can be called from UI thread. Use with care.
+ GCMClient* gcm_client_for_testing() const { return gcm_client_.get(); }
+
+ private:
+ scoped_refptr<base::SequencedTaskRunner> ui_thread_;
+ scoped_refptr<base::SequencedTaskRunner> io_thread_;
+
+ base::WeakPtr<GCMDriverDesktop> service_;
+
+ std::unique_ptr<GCMClient> gcm_client_;
+};
+
+GCMDriverDesktop::IOWorker::IOWorker(
+ const scoped_refptr<base::SequencedTaskRunner>& ui_thread,
+ const scoped_refptr<base::SequencedTaskRunner>& io_thread)
+ : ui_thread_(ui_thread),
+ io_thread_(io_thread) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+}
+
+GCMDriverDesktop::IOWorker::~IOWorker() {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+}
+
+void GCMDriverDesktop::IOWorker::Initialize(
+ std::unique_ptr<GCMClientFactory> gcm_client_factory,
+ const GCMClient::ChromeBuildInfo& chrome_build_info,
+ const base::FilePath& store_path,
+ bool remove_account_mappings_with_email_key,
+ base::RepeatingCallback<void(
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)>
+ get_socket_factory_callback,
+ std::unique_ptr<network::PendingSharedURLLoaderFactory>
+ pending_loader_factory,
+ network::NetworkConnectionTracker* network_connection_tracker,
+ const scoped_refptr<base::SequencedTaskRunner> blocking_task_runner) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ gcm_client_ = gcm_client_factory->BuildInstance();
+
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_for_io =
+ network::SharedURLLoaderFactory::Create(
+ std::move(pending_loader_factory));
+
+ gcm_client_->Initialize(
+ chrome_build_info, store_path, remove_account_mappings_with_email_key,
+ blocking_task_runner, io_thread_, std::move(get_socket_factory_callback),
+ url_loader_factory_for_io, network_connection_tracker,
+ std::make_unique<SystemEncryptor>(), this);
+}
+
+void GCMDriverDesktop::IOWorker::OnRegisterFinished(
+ scoped_refptr<RegistrationInfo> registration_info,
+ const std::string& registration_id,
+ GCMClient::Result result) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ const GCMRegistrationInfo* gcm_registration_info =
+ GCMRegistrationInfo::FromRegistrationInfo(registration_info.get());
+ if (gcm_registration_info) {
+ ui_thread_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&GCMDriverDesktop::RegisterFinished, service_,
+ gcm_registration_info->app_id, registration_id, result));
+ }
+
+ const InstanceIDTokenInfo* instance_id_token_info =
+ InstanceIDTokenInfo::FromRegistrationInfo(registration_info.get());
+ if (instance_id_token_info) {
+ ui_thread_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&GCMDriverDesktop::GetTokenFinished, service_,
+ instance_id_token_info->app_id,
+ instance_id_token_info->authorized_entity,
+ instance_id_token_info->scope, registration_id, result));
+ }
+}
+
+void GCMDriverDesktop::IOWorker::OnUnregisterFinished(
+ scoped_refptr<RegistrationInfo> registration_info,
+ GCMClient::Result result) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ const GCMRegistrationInfo* gcm_registration_info =
+ GCMRegistrationInfo::FromRegistrationInfo(registration_info.get());
+ if (gcm_registration_info) {
+ ui_thread_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&GCMDriverDesktop::RemoveEncryptionInfoAfterUnregister,
+ service_, gcm_registration_info->app_id, result));
+ }
+
+ const InstanceIDTokenInfo* instance_id_token_info =
+ InstanceIDTokenInfo::FromRegistrationInfo(registration_info.get());
+ if (instance_id_token_info) {
+ ui_thread_->PostTask(
+ FROM_HERE, base::BindOnce(&GCMDriverDesktop::DeleteTokenFinished,
+ service_, instance_id_token_info->app_id,
+ instance_id_token_info->authorized_entity,
+ instance_id_token_info->scope, result));
+ }
+}
+
+void GCMDriverDesktop::IOWorker::OnSendFinished(const std::string& app_id,
+ const std::string& message_id,
+ GCMClient::Result result) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ ui_thread_->PostTask(FROM_HERE,
+ base::BindOnce(&GCMDriverDesktop::SendFinished, service_,
+ app_id, message_id, result));
+}
+
+void GCMDriverDesktop::IOWorker::OnMessageReceived(
+ const std::string& app_id,
+ const IncomingMessage& message) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ ui_thread_->PostTask(
+ FROM_HERE, base::BindOnce(&GCMDriverDesktop::MessageReceived, service_,
+ app_id, message));
+}
+
+void GCMDriverDesktop::IOWorker::OnMessagesDeleted(const std::string& app_id) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ ui_thread_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&GCMDriverDesktop::MessagesDeleted, service_, app_id));
+}
+
+void GCMDriverDesktop::IOWorker::OnMessageSendError(
+ const std::string& app_id,
+ const GCMClient::SendErrorDetails& send_error_details) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ ui_thread_->PostTask(
+ FROM_HERE, base::BindOnce(&GCMDriverDesktop::MessageSendError, service_,
+ app_id, send_error_details));
+}
+
+void GCMDriverDesktop::IOWorker::OnSendAcknowledged(
+ const std::string& app_id,
+ const std::string& message_id) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ ui_thread_->PostTask(
+ FROM_HERE, base::BindOnce(&GCMDriverDesktop::SendAcknowledged, service_,
+ app_id, message_id));
+}
+
+void GCMDriverDesktop::IOWorker::OnGCMReady(
+ const std::vector<AccountMapping>& account_mappings,
+ const base::Time& last_token_fetch_time) {
+ ui_thread_->PostTask(
+ FROM_HERE, base::BindOnce(&GCMDriverDesktop::GCMClientReady, service_,
+ account_mappings, last_token_fetch_time));
+}
+
+void GCMDriverDesktop::IOWorker::OnActivityRecorded() {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+ // When an activity is recorded, get all the stats and refresh the UI of
+ // gcm-internals page.
+ gcm::GCMClient::GCMStatistics stats;
+ if (gcm_client_) {
+ stats = gcm_client_->GetStatistics();
+ }
+ ui_thread_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&GCMDriverDesktop::OnActivityRecorded, service_, stats));
+}
+
+void GCMDriverDesktop::IOWorker::OnConnected(
+ const net::IPEndPoint& ip_endpoint) {
+ ui_thread_->PostTask(FROM_HERE, base::BindOnce(&GCMDriverDesktop::OnConnected,
+ service_, ip_endpoint));
+}
+
+void GCMDriverDesktop::IOWorker::OnDisconnected() {
+ ui_thread_->PostTask(
+ FROM_HERE, base::BindOnce(&GCMDriverDesktop::OnDisconnected, service_));
+}
+
+void GCMDriverDesktop::IOWorker::OnStoreReset() {
+ ui_thread_->PostTask(
+ FROM_HERE, base::BindOnce(&GCMDriverDesktop::OnStoreReset, service_));
+}
+
+void GCMDriverDesktop::IOWorker::Start(
+ GCMClient::StartMode start_mode,
+ const base::WeakPtr<GCMDriverDesktop>& service) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ service_ = service;
+ gcm_client_->Start(start_mode);
+}
+
+void GCMDriverDesktop::IOWorker::Stop() {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ gcm_client_->Stop();
+}
+
+void GCMDriverDesktop::IOWorker::Register(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ auto gcm_info = base::MakeRefCounted<GCMRegistrationInfo>();
+ gcm_info->app_id = app_id;
+ gcm_info->sender_ids = sender_ids;
+ gcm_client_->Register(std::move(gcm_info));
+}
+
+bool GCMDriverDesktop::IOWorker::ValidateRegistration(
+ scoped_refptr<RegistrationInfo> registration_info,
+ const std::string& registration_id) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ return gcm_client_->ValidateRegistration(std::move(registration_info),
+ registration_id);
+}
+
+void GCMDriverDesktop::IOWorker::Unregister(const std::string& app_id) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ auto gcm_info = base::MakeRefCounted<GCMRegistrationInfo>();
+ gcm_info->app_id = app_id;
+ gcm_client_->Unregister(std::move(gcm_info));
+}
+
+void GCMDriverDesktop::IOWorker::Send(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ gcm_client_->Send(app_id, receiver_id, message);
+}
+
+void GCMDriverDesktop::IOWorker::GetGCMStatistics(
+ GetGCMStatisticsCallback callback,
+ ClearActivityLogs clear_logs) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+ gcm::GCMClient::GCMStatistics stats;
+
+ if (gcm_client_) {
+ if (clear_logs == GCMDriver::CLEAR_LOGS)
+ gcm_client_->ClearActivityLogs();
+ stats = gcm_client_->GetStatistics();
+ }
+
+ ui_thread_->PostTask(FROM_HERE, base::BindOnce(std::move(callback), stats));
+}
+
+void GCMDriverDesktop::IOWorker::SetGCMRecording(
+ GetGCMStatisticsCallback callback,
+ bool recording) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+ gcm::GCMClient::GCMStatistics stats;
+
+ if (gcm_client_) {
+ gcm_client_->SetRecording(recording);
+ stats = gcm_client_->GetStatistics();
+ stats.gcm_client_created = true;
+ }
+
+ ui_thread_->PostTask(FROM_HERE, base::BindOnce(std::move(callback), stats));
+}
+
+void GCMDriverDesktop::IOWorker::SetAccountTokens(
+ const std::vector<GCMClient::AccountTokenInfo>& account_tokens) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ if (gcm_client_)
+ gcm_client_->SetAccountTokens(account_tokens);
+}
+
+void GCMDriverDesktop::IOWorker::UpdateAccountMapping(
+ const AccountMapping& account_mapping) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ if (gcm_client_)
+ gcm_client_->UpdateAccountMapping(account_mapping);
+}
+
+void GCMDriverDesktop::IOWorker::RemoveAccountMapping(
+ const CoreAccountId& account_id) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ if (gcm_client_)
+ gcm_client_->RemoveAccountMapping(account_id);
+}
+
+void GCMDriverDesktop::IOWorker::SetLastTokenFetchTime(const base::Time& time) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ if (gcm_client_)
+ gcm_client_->SetLastTokenFetchTime(time);
+}
+
+void GCMDriverDesktop::IOWorker::AddInstanceIDData(
+ const std::string& app_id,
+ const std::string& instance_id,
+ const std::string& extra_data) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ if (gcm_client_)
+ gcm_client_->AddInstanceIDData(app_id, instance_id, extra_data);
+}
+
+void GCMDriverDesktop::IOWorker::RemoveInstanceIDData(
+ const std::string& app_id) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ if (gcm_client_)
+ gcm_client_->RemoveInstanceIDData(app_id);
+}
+
+void GCMDriverDesktop::IOWorker::GetInstanceIDData(
+ const std::string& app_id) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ std::string instance_id;
+ std::string extra_data;
+ if (gcm_client_)
+ gcm_client_->GetInstanceIDData(app_id, &instance_id, &extra_data);
+
+ ui_thread_->PostTask(
+ FROM_HERE, base::BindOnce(&GCMDriverDesktop::GetInstanceIDDataFinished,
+ service_, app_id, instance_id, extra_data));
+}
+
+void GCMDriverDesktop::IOWorker::GetToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ base::TimeDelta time_to_live) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+
+ auto instance_id_token_info = base::MakeRefCounted<InstanceIDTokenInfo>();
+ instance_id_token_info->app_id = app_id;
+ instance_id_token_info->authorized_entity = authorized_entity;
+ instance_id_token_info->scope = scope;
+ instance_id_token_info->time_to_live = time_to_live;
+ gcm_client_->Register(std::move(instance_id_token_info));
+}
+
+void GCMDriverDesktop::IOWorker::DeleteToken(
+ const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope) {
+ auto instance_id_token_info = base::MakeRefCounted<InstanceIDTokenInfo>();
+ instance_id_token_info->app_id = app_id;
+ instance_id_token_info->authorized_entity = authorized_entity;
+ instance_id_token_info->scope = scope;
+ gcm_client_->Unregister(std::move(instance_id_token_info));
+}
+
+void GCMDriverDesktop::IOWorker::AddHeartbeatInterval(const std::string& scope,
+ int interval_ms) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+ gcm_client_->AddHeartbeatInterval(scope, interval_ms);
+}
+
+void GCMDriverDesktop::IOWorker::RemoveHeartbeatInterval(
+ const std::string& scope) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+ gcm_client_->RemoveHeartbeatInterval(scope);
+}
+
+void GCMDriverDesktop::IOWorker::RecordDecryptionFailure(
+ const std::string& app_id,
+ GCMDecryptionResult result) {
+ DCHECK(io_thread_->RunsTasksInCurrentSequence());
+ gcm_client_->RecordDecryptionFailure(app_id, result);
+}
+
+GCMDriverDesktop::GCMDriverDesktop(
+ std::unique_ptr<GCMClientFactory> gcm_client_factory,
+ const GCMClient::ChromeBuildInfo& chrome_build_info,
+ PrefService* prefs,
+ const base::FilePath& store_path,
+ bool remove_account_mappings_with_email_key,
+ base::RepeatingCallback<void(
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)>
+ get_socket_factory_callback,
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_for_ui,
+ network::NetworkConnectionTracker* network_connection_tracker,
+ const scoped_refptr<base::SequencedTaskRunner>& ui_thread,
+ const scoped_refptr<base::SequencedTaskRunner>& io_thread,
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner)
+ : GCMDriver(store_path, blocking_task_runner),
+ signed_in_(false),
+ gcm_started_(false),
+ connected_(false),
+ account_mapper_(new GCMAccountMapper(this)),
+ // Setting to max, to make sure it does not prompt for token reporting
+ // Before reading a reasonable value from the DB, which might be never,
+ // in which case the fetching will be triggered.
+ last_token_fetch_time_(base::Time::Max()),
+ ui_thread_(ui_thread),
+ io_thread_(io_thread) {
+ // Create and initialize the GCMClient. Note that this does not initiate the
+ // GCM check-in.
+ io_worker_ = std::make_unique<IOWorker>(ui_thread, io_thread);
+ io_thread_->PostTask(
+ FROM_HERE,
+ base::BindOnce(
+ &GCMDriverDesktop::IOWorker::Initialize,
+ base::Unretained(io_worker_.get()), std::move(gcm_client_factory),
+ chrome_build_info, store_path, remove_account_mappings_with_email_key,
+ std::move(get_socket_factory_callback),
+ // ->Clone() permits creation of an equivalent
+ // SharedURLLoaderFactory on IO thread.
+ url_loader_factory_for_ui->Clone(),
+ base::Unretained(network_connection_tracker), blocking_task_runner));
+}
+
+GCMDriverDesktop::~GCMDriverDesktop() {
+}
+
+void GCMDriverDesktop::ValidateRegistration(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ const std::string& registration_id,
+ ValidateRegistrationCallback callback) {
+ DCHECK(!app_id.empty());
+ DCHECK(!sender_ids.empty() && sender_ids.size() <= kMaxSenders);
+ DCHECK(!registration_id.empty());
+ DCHECK(!callback.is_null());
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START);
+ if (result != GCMClient::SUCCESS) {
+ // Can't tell whether the registration is valid or not, so don't run the
+ // callback (let it hang indefinitely).
+ return;
+ }
+
+ // Only validating current state, so ignore pending register_callbacks_.
+
+ auto gcm_info = base::MakeRefCounted<GCMRegistrationInfo>();
+ gcm_info->app_id = app_id;
+ gcm_info->sender_ids = sender_ids;
+ // Normalize the sender IDs by making them sorted.
+ std::sort(gcm_info->sender_ids.begin(), gcm_info->sender_ids.end());
+
+ if (!delayed_task_controller_->CanRunTaskWithoutDelay()) {
+ delayed_task_controller_->AddTask(
+ base::BindOnce(&GCMDriverDesktop::DoValidateRegistration,
+ weak_ptr_factory_.GetWeakPtr(), gcm_info,
+ registration_id, std::move(callback)));
+ return;
+ }
+
+ DoValidateRegistration(std::move(gcm_info), registration_id,
+ std::move(callback));
+}
+
+void GCMDriverDesktop::DoValidateRegistration(
+ scoped_refptr<RegistrationInfo> registration_info,
+ const std::string& registration_id,
+ ValidateRegistrationCallback callback) {
+ base::PostTaskAndReplyWithResult(
+ io_thread_.get(), FROM_HERE,
+ base::BindOnce(&GCMDriverDesktop::IOWorker::ValidateRegistration,
+ base::Unretained(io_worker_.get()),
+ std::move(registration_info), registration_id),
+ std::move(callback));
+}
+
+void GCMDriverDesktop::Shutdown() {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ Stop();
+ GCMDriver::Shutdown();
+
+ io_thread_->DeleteSoon(FROM_HERE, io_worker_.release());
+}
+
+void GCMDriverDesktop::OnSignedIn() {
+ signed_in_ = true;
+}
+
+void GCMDriverDesktop::OnSignedOut() {
+ signed_in_ = false;
+}
+
+void GCMDriverDesktop::AddAppHandler(const std::string& app_id,
+ GCMAppHandler* handler) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+ GCMDriver::AddAppHandler(app_id, handler);
+
+ // Ensures that the GCM service is started when there is an interest.
+ EnsureStarted(GCMClient::DELAYED_START);
+}
+
+void GCMDriverDesktop::RemoveAppHandler(const std::string& app_id) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+ GCMDriver::RemoveAppHandler(app_id);
+
+ // Stops the GCM service when no app intends to consume it. Stop function will
+ // remove the last app handler - account mapper.
+ if (app_handlers().size() == 1)
+ Stop();
+}
+
+void GCMDriverDesktop::AddConnectionObserver(GCMConnectionObserver* observer) {
+ connection_observer_list_.AddObserver(observer);
+}
+
+void GCMDriverDesktop::RemoveConnectionObserver(
+ GCMConnectionObserver* observer) {
+ connection_observer_list_.RemoveObserver(observer);
+}
+
+void GCMDriverDesktop::Stop() {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ // No need to stop GCM service if not started yet.
+ if (!gcm_started_)
+ return;
+
+ account_mapper_->ShutdownHandler();
+ GCMDriver::RemoveAppHandler(kGCMAccountMapperAppId);
+
+ RemoveCachedData();
+
+ io_thread_->PostTask(FROM_HERE,
+ base::BindOnce(&GCMDriverDesktop::IOWorker::Stop,
+ base::Unretained(io_worker_.get())));
+}
+
+void GCMDriverDesktop::RegisterImpl(
+ const std::string& app_id,
+ const std::vector<std::string>& sender_ids) {
+ // Delay the register operation until GCMClient is ready.
+ if (!delayed_task_controller_->CanRunTaskWithoutDelay()) {
+ delayed_task_controller_->AddTask(
+ base::BindOnce(&GCMDriverDesktop::DoRegister,
+ weak_ptr_factory_.GetWeakPtr(), app_id, sender_ids));
+ return;
+ }
+
+ DoRegister(app_id, sender_ids);
+}
+
+void GCMDriverDesktop::DoRegister(const std::string& app_id,
+ const std::vector<std::string>& sender_ids) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+ if (!HasRegisterCallback(app_id)) {
+ // The callback could have been removed when the app is uninstalled.
+ return;
+ }
+
+ io_thread_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&GCMDriverDesktop::IOWorker::Register,
+ base::Unretained(io_worker_.get()), app_id, sender_ids));
+}
+
+void GCMDriverDesktop::UnregisterImpl(const std::string& app_id) {
+ // Delay the unregister operation until GCMClient is ready.
+ if (!delayed_task_controller_->CanRunTaskWithoutDelay()) {
+ delayed_task_controller_->AddTask(
+ base::BindOnce(&GCMDriverDesktop::DoUnregister,
+ weak_ptr_factory_.GetWeakPtr(), app_id));
+ return;
+ }
+
+ DoUnregister(app_id);
+}
+
+void GCMDriverDesktop::DoUnregister(const std::string& app_id) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ // Ask the server to unregister it. There could be a small chance that the
+ // unregister request fails. If this occurs, it does not bring any harm since
+ // we simply reject the messages/events received from the server.
+ io_thread_->PostTask(
+ FROM_HERE, base::BindOnce(&GCMDriverDesktop::IOWorker::Unregister,
+ base::Unretained(io_worker_.get()), app_id));
+}
+
+void GCMDriverDesktop::SendImpl(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) {
+ // Delay the send operation until all GCMClient is ready.
+ if (!delayed_task_controller_->CanRunTaskWithoutDelay()) {
+ delayed_task_controller_->AddTask(base::BindOnce(
+ &GCMDriverDesktop::DoSend, weak_ptr_factory_.GetWeakPtr(), app_id,
+ receiver_id, message));
+ return;
+ }
+
+ DoSend(app_id, receiver_id, message);
+}
+
+void GCMDriverDesktop::DoSend(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+ io_thread_->PostTask(
+ FROM_HERE, base::BindOnce(&GCMDriverDesktop::IOWorker::Send,
+ base::Unretained(io_worker_.get()), app_id,
+ receiver_id, message));
+}
+
+void GCMDriverDesktop::RecordDecryptionFailure(const std::string& app_id,
+ GCMDecryptionResult result) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+ io_thread_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&GCMDriverDesktop::IOWorker::RecordDecryptionFailure,
+ base::Unretained(io_worker_.get()), app_id, result));
+}
+
+GCMClient* GCMDriverDesktop::GetGCMClientForTesting() const {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+ return io_worker_ ? io_worker_->gcm_client_for_testing() : nullptr;
+}
+
+bool GCMDriverDesktop::IsStarted() const {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+ return gcm_started_;
+}
+
+bool GCMDriverDesktop::IsConnected() const {
+ return connected_;
+}
+
+void GCMDriverDesktop::GetGCMStatistics(GetGCMStatisticsCallback callback,
+ ClearActivityLogs clear_logs) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+ DCHECK(!callback.is_null());
+
+ io_thread_->PostTask(
+ FROM_HERE, base::BindOnce(&GCMDriverDesktop::IOWorker::GetGCMStatistics,
+ base::Unretained(io_worker_.get()),
+ std::move(callback), clear_logs));
+}
+
+void GCMDriverDesktop::SetGCMRecording(
+ const GCMStatisticsRecordingCallback& callback,
+ bool recording) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ gcm_statistics_recording_callback_ = callback;
+ io_thread_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&GCMDriverDesktop::IOWorker::SetGCMRecording,
+ base::Unretained(io_worker_.get()), callback, recording));
+}
+
+void GCMDriverDesktop::UpdateAccountMapping(
+ const AccountMapping& account_mapping) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ io_thread_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&GCMDriverDesktop::IOWorker::UpdateAccountMapping,
+ base::Unretained(io_worker_.get()), account_mapping));
+}
+
+void GCMDriverDesktop::RemoveAccountMapping(const CoreAccountId& account_id) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ io_thread_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&GCMDriverDesktop::IOWorker::RemoveAccountMapping,
+ base::Unretained(io_worker_.get()), account_id));
+}
+
+base::Time GCMDriverDesktop::GetLastTokenFetchTime() {
+ return last_token_fetch_time_;
+}
+
+void GCMDriverDesktop::SetLastTokenFetchTime(const base::Time& time) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ last_token_fetch_time_ = time;
+
+ io_thread_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&GCMDriverDesktop::IOWorker::SetLastTokenFetchTime,
+ base::Unretained(io_worker_.get()), time));
+}
+
+InstanceIDHandler* GCMDriverDesktop::GetInstanceIDHandlerInternal() {
+ return this;
+}
+
+void GCMDriverDesktop::GetToken(
+ const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ base::TimeDelta time_to_live,
+ GetTokenCallback callback) {
+ DCHECK(!app_id.empty());
+ DCHECK(!authorized_entity.empty());
+ DCHECK(!scope.empty());
+ DCHECK(!callback.is_null());
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START);
+ if (result != GCMClient::SUCCESS) {
+ DLOG(ERROR)
+ << "Unable to get the InstanceID token: cannot start the GCM Client";
+
+ std::move(callback).Run(std::string(), result);
+ return;
+ }
+
+ // If previous GetToken operation is still in progress, bail out.
+ TokenTuple tuple_key(app_id, authorized_entity, scope);
+ if (get_token_callbacks_.find(tuple_key) != get_token_callbacks_.end()) {
+ std::move(callback).Run(std::string(), GCMClient::ASYNC_OPERATION_PENDING);
+ return;
+ }
+
+ get_token_callbacks_[tuple_key] = std::move(callback);
+
+ // Delay the GetToken operation until GCMClient is ready.
+ if (!delayed_task_controller_->CanRunTaskWithoutDelay()) {
+ delayed_task_controller_->AddTask(base::BindOnce(
+ &GCMDriverDesktop::DoGetToken, weak_ptr_factory_.GetWeakPtr(), app_id,
+ authorized_entity, scope, time_to_live));
+ return;
+ }
+
+ DoGetToken(app_id, authorized_entity, scope, time_to_live);
+}
+
+void GCMDriverDesktop::DoGetToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ base::TimeDelta time_to_live) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ TokenTuple tuple_key(app_id, authorized_entity, scope);
+ auto callback_iter = get_token_callbacks_.find(tuple_key);
+ if (callback_iter == get_token_callbacks_.end()) {
+ // The callback could have been removed when the app is uninstalled.
+ return;
+ }
+
+ io_thread_->PostTask(
+ FROM_HERE, base::BindOnce(&GCMDriverDesktop::IOWorker::GetToken,
+ base::Unretained(io_worker_.get()), app_id,
+ authorized_entity, scope, time_to_live));
+}
+
+void GCMDriverDesktop::ValidateToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ const std::string& token,
+ ValidateTokenCallback callback) {
+ DCHECK(!app_id.empty());
+ DCHECK(!authorized_entity.empty());
+ DCHECK(!scope.empty());
+ DCHECK(!token.empty());
+ DCHECK(callback);
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START);
+ if (result != GCMClient::SUCCESS) {
+ // Can't tell whether the registration is valid or not, so don't run the
+ // callback (let it hang indefinitely).
+ DLOG(ERROR) << "Unable to validate the InstanceID token: cannot start the "
+ "GCM Client";
+ return;
+ }
+
+ // Only validating current state, so ignore pending get_token_callbacks_.
+
+ auto instance_id_info = base::MakeRefCounted<InstanceIDTokenInfo>();
+ instance_id_info->app_id = app_id;
+ instance_id_info->authorized_entity = authorized_entity;
+ instance_id_info->scope = scope;
+
+ if (!delayed_task_controller_->CanRunTaskWithoutDelay()) {
+ delayed_task_controller_->AddTask(
+ base::BindOnce(&GCMDriverDesktop::DoValidateRegistration,
+ weak_ptr_factory_.GetWeakPtr(), instance_id_info, token,
+ std::move(callback)));
+ return;
+ }
+
+ DoValidateRegistration(std::move(instance_id_info), token,
+ std::move(callback));
+}
+
+void GCMDriverDesktop::DeleteToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ DeleteTokenCallback callback) {
+ DCHECK(!app_id.empty());
+ DCHECK(!authorized_entity.empty());
+ DCHECK(!scope.empty());
+ DCHECK(!callback.is_null());
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START);
+ if (result != GCMClient::SUCCESS) {
+ DLOG(ERROR)
+ << "Unable to delete the InstanceID token: cannot start the GCM Client";
+
+ std::move(callback).Run(result);
+ return;
+ }
+
+ // If previous GetToken operation is still in progress, bail out.
+ TokenTuple tuple_key(app_id, authorized_entity, scope);
+ if (delete_token_callbacks_.find(tuple_key) !=
+ delete_token_callbacks_.end()) {
+ std::move(callback).Run(GCMClient::ASYNC_OPERATION_PENDING);
+ return;
+ }
+
+ delete_token_callbacks_[tuple_key] = std::move(callback);
+
+ // Delay the DeleteToken operation until GCMClient is ready.
+ if (!delayed_task_controller_->CanRunTaskWithoutDelay()) {
+ delayed_task_controller_->AddTask(base::BindOnce(
+ &GCMDriverDesktop::DoDeleteToken, weak_ptr_factory_.GetWeakPtr(),
+ app_id, authorized_entity, scope));
+ return;
+ }
+
+ DoDeleteToken(app_id, authorized_entity, scope);
+}
+
+void GCMDriverDesktop::DoDeleteToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ io_thread_->PostTask(
+ FROM_HERE, base::BindOnce(&GCMDriverDesktop::IOWorker::DeleteToken,
+ base::Unretained(io_worker_.get()), app_id,
+ authorized_entity, scope));
+}
+
+void GCMDriverDesktop::AddInstanceIDData(
+ const std::string& app_id,
+ const std::string& instance_id,
+ const std::string& extra_data) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START);
+ if (result != GCMClient::SUCCESS) {
+ DLOG(ERROR)
+ << "Unable to add the InstanceID data: cannot start the GCM Client";
+ return;
+ }
+
+ // Delay the operation until GCMClient is ready.
+ if (!delayed_task_controller_->CanRunTaskWithoutDelay()) {
+ delayed_task_controller_->AddTask(base::BindOnce(
+ &GCMDriverDesktop::DoAddInstanceIDData, weak_ptr_factory_.GetWeakPtr(),
+ app_id, instance_id, extra_data));
+ return;
+ }
+
+ DoAddInstanceIDData(app_id, instance_id, extra_data);
+}
+
+void GCMDriverDesktop::DoAddInstanceIDData(
+ const std::string& app_id,
+ const std::string& instance_id,
+ const std::string& extra_data) {
+ io_thread_->PostTask(
+ FROM_HERE, base::BindOnce(&GCMDriverDesktop::IOWorker::AddInstanceIDData,
+ base::Unretained(io_worker_.get()), app_id,
+ instance_id, extra_data));
+}
+
+void GCMDriverDesktop::RemoveInstanceIDData(const std::string& app_id) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START);
+ if (result != GCMClient::SUCCESS) {
+ DLOG(ERROR)
+ << "Unable to remove the InstanceID data: cannot start the GCM Client";
+ return;
+ }
+
+ // Delay the operation until GCMClient is ready.
+ if (!delayed_task_controller_->CanRunTaskWithoutDelay()) {
+ delayed_task_controller_->AddTask(
+ base::BindOnce(&GCMDriverDesktop::DoRemoveInstanceIDData,
+ weak_ptr_factory_.GetWeakPtr(), app_id));
+ return;
+ }
+
+ DoRemoveInstanceIDData(app_id);
+}
+
+void GCMDriverDesktop::DoRemoveInstanceIDData(const std::string& app_id) {
+ io_thread_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&GCMDriverDesktop::IOWorker::RemoveInstanceIDData,
+ base::Unretained(io_worker_.get()), app_id));
+}
+
+void GCMDriverDesktop::GetInstanceIDData(const std::string& app_id,
+ GetInstanceIDDataCallback callback) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ GCMClient::Result result = EnsureStarted(GCMClient::IMMEDIATE_START);
+ // TODO(crbug/1028761): This method is only used by InstanceIDImpl to get the
+ // current instance ID from the store. As this method doesn't support error
+ // codes, the instance ID will assume no current ID and generate a new one
+ // if the gcm client is not ready and we pass an empty string to the callback
+ // below. We should fix this!
+ if (result != GCMClient::SUCCESS) {
+ DLOG(ERROR)
+ << "Unable to get the InstanceID data: cannot start the GCM Client";
+ // Resolve the |callback| to not leave it hanging indefinitely.
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(std::move(callback), std::string(), std::string()));
+ return;
+ }
+
+ get_instance_id_data_callbacks_[app_id].push(std::move(callback));
+
+ // Delay the operation until GCMClient is ready.
+ if (!delayed_task_controller_->CanRunTaskWithoutDelay()) {
+ delayed_task_controller_->AddTask(
+ base::BindOnce(&GCMDriverDesktop::DoGetInstanceIDData,
+ weak_ptr_factory_.GetWeakPtr(), app_id));
+ return;
+ }
+
+ DoGetInstanceIDData(app_id);
+}
+
+void GCMDriverDesktop::DoGetInstanceIDData(const std::string& app_id) {
+ io_thread_->PostTask(
+ FROM_HERE, base::BindOnce(&GCMDriverDesktop::IOWorker::GetInstanceIDData,
+ base::Unretained(io_worker_.get()), app_id));
+}
+
+void GCMDriverDesktop::GetInstanceIDDataFinished(
+ const std::string& app_id,
+ const std::string& instance_id,
+ const std::string& extra_data) {
+ auto iter = get_instance_id_data_callbacks_.find(app_id);
+ DCHECK(iter != get_instance_id_data_callbacks_.end());
+
+ base::queue<GetInstanceIDDataCallback>& callbacks = iter->second;
+ std::move(callbacks.front()).Run(instance_id, extra_data);
+
+ callbacks.pop();
+
+ if (!callbacks.size())
+ get_instance_id_data_callbacks_.erase(iter);
+}
+
+void GCMDriverDesktop::GetTokenFinished(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ const std::string& token,
+ GCMClient::Result result) {
+ TokenTuple tuple_key(app_id, authorized_entity, scope);
+ auto callback_iter = get_token_callbacks_.find(tuple_key);
+ if (callback_iter == get_token_callbacks_.end()) {
+ // The callback could have been removed when the app is uninstalled.
+ return;
+ }
+
+ GetTokenCallback callback = std::move(callback_iter->second);
+ get_token_callbacks_.erase(callback_iter);
+ std::move(callback).Run(token, result);
+}
+
+void GCMDriverDesktop::DeleteTokenFinished(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ GCMClient::Result result) {
+ TokenTuple tuple_key(app_id, authorized_entity, scope);
+ auto callback_iter = delete_token_callbacks_.find(tuple_key);
+ if (callback_iter == delete_token_callbacks_.end()) {
+ // The callback could have been removed when the app is uninstalled.
+ return;
+ }
+
+ DeleteTokenCallback callback = std::move(callback_iter->second);
+ delete_token_callbacks_.erase(callback_iter);
+ std::move(callback).Run(result);
+}
+
+void GCMDriverDesktop::AddHeartbeatInterval(const std::string& scope,
+ int interval_ms) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ // The GCM service has not been initialized.
+ if (!delayed_task_controller_)
+ return;
+
+ if (!delayed_task_controller_->CanRunTaskWithoutDelay()) {
+ // The GCM service was initialized but has not started yet.
+ delayed_task_controller_->AddTask(
+ base::BindOnce(&GCMDriverDesktop::AddHeartbeatInterval,
+ weak_ptr_factory_.GetWeakPtr(), scope, interval_ms));
+ return;
+ }
+
+ io_thread_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&GCMDriverDesktop::IOWorker::AddHeartbeatInterval,
+ base::Unretained(io_worker_.get()), scope, interval_ms));
+}
+
+void GCMDriverDesktop::RemoveHeartbeatInterval(const std::string& scope) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ // The GCM service has not been initialized.
+ if (!delayed_task_controller_)
+ return;
+
+ if (!delayed_task_controller_->CanRunTaskWithoutDelay()) {
+ // The GCM service was initialized but has not started yet.
+ delayed_task_controller_->AddTask(
+ base::BindOnce(&GCMDriverDesktop::RemoveHeartbeatInterval,
+ weak_ptr_factory_.GetWeakPtr(), scope));
+ return;
+ }
+
+ io_thread_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&GCMDriverDesktop::IOWorker::RemoveHeartbeatInterval,
+ base::Unretained(io_worker_.get()), scope));
+}
+
+void GCMDriverDesktop::SetAccountTokens(
+ const std::vector<GCMClient::AccountTokenInfo>& account_tokens) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ account_mapper_->SetAccountTokens(account_tokens);
+
+ io_thread_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&GCMDriverDesktop::IOWorker::SetAccountTokens,
+ base::Unretained(io_worker_.get()), account_tokens));
+}
+
+GCMClient::Result GCMDriverDesktop::EnsureStarted(
+ GCMClient::StartMode start_mode) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ if (gcm_started_)
+ return GCMClient::SUCCESS;
+
+ // Have any app requested the service?
+ if (app_handlers().empty())
+ return GCMClient::UNKNOWN_ERROR;
+
+ if (!delayed_task_controller_)
+ delayed_task_controller_ = std::make_unique<GCMDelayedTaskController>();
+
+ // Note that we need to pass weak pointer again since the existing weak
+ // pointer in IOWorker might have been invalidated when GCM is stopped.
+ io_thread_->PostTask(
+ FROM_HERE, base::BindOnce(&GCMDriverDesktop::IOWorker::Start,
+ base::Unretained(io_worker_.get()), start_mode,
+ weak_ptr_factory_.GetWeakPtr()));
+
+ return GCMClient::SUCCESS;
+}
+
+void GCMDriverDesktop::RemoveCachedData() {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+ // Remove all the queued tasks since they no longer make sense after
+ // GCM service is stopped.
+ weak_ptr_factory_.InvalidateWeakPtrs();
+
+ gcm_started_ = false;
+ delayed_task_controller_.reset();
+ ClearCallbacks();
+}
+
+void GCMDriverDesktop::MessageReceived(const std::string& app_id,
+ const IncomingMessage& message) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ // Drop the event if the service has been stopped.
+ if (!gcm_started_)
+ return;
+
+ DispatchMessage(app_id, message);
+}
+
+void GCMDriverDesktop::MessagesDeleted(const std::string& app_id) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ // Drop the event if the service has been stopped.
+ if (!gcm_started_)
+ return;
+
+ GCMAppHandler* handler = GetAppHandler(app_id);
+ if (handler)
+ handler->OnMessagesDeleted(app_id);
+}
+
+void GCMDriverDesktop::MessageSendError(
+ const std::string& app_id,
+ const GCMClient::SendErrorDetails& send_error_details) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ // Drop the event if the service has been stopped.
+ if (!gcm_started_)
+ return;
+
+ GCMAppHandler* handler = GetAppHandler(app_id);
+ if (handler)
+ handler->OnSendError(app_id, send_error_details);
+}
+
+void GCMDriverDesktop::SendAcknowledged(const std::string& app_id,
+ const std::string& message_id) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ // Drop the event if the service has been stopped.
+ if (!gcm_started_)
+ return;
+
+ GCMAppHandler* handler = GetAppHandler(app_id);
+ if (handler)
+ handler->OnSendAcknowledged(app_id, message_id);
+}
+
+void GCMDriverDesktop::GCMClientReady(
+ const std::vector<AccountMapping>& account_mappings,
+ const base::Time& last_token_fetch_time) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ UMA_HISTOGRAM_BOOLEAN("GCM.UserSignedIn", signed_in_);
+
+ gcm_started_ = true;
+
+ last_token_fetch_time_ = last_token_fetch_time;
+
+ GCMDriver::AddAppHandler(kGCMAccountMapperAppId, account_mapper_.get());
+ account_mapper_->Initialize(
+ account_mappings, base::BindRepeating(&GCMDriverDesktop::MessageReceived,
+ weak_ptr_factory_.GetWeakPtr()));
+
+ delayed_task_controller_->SetReady();
+}
+
+void GCMDriverDesktop::OnConnected(const net::IPEndPoint& ip_endpoint) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ connected_ = true;
+
+ // Drop the event if the service has been stopped.
+ if (!gcm_started_)
+ return;
+
+ for (GCMConnectionObserver& observer : connection_observer_list_)
+ observer.OnConnected(ip_endpoint);
+}
+
+void GCMDriverDesktop::OnDisconnected() {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ connected_ = false;
+
+ // Drop the event if the service has been stopped.
+ if (!gcm_started_)
+ return;
+
+ for (GCMConnectionObserver& observer : connection_observer_list_)
+ observer.OnDisconnected();
+}
+
+void GCMDriverDesktop::OnStoreReset() {
+ // Defensive copy in case OnStoreReset calls Add/RemoveAppHandler.
+ std::vector<GCMAppHandler*> app_handler_values;
+ for (const auto& key_value : app_handlers())
+ app_handler_values.push_back(key_value.second);
+ for (GCMAppHandler* app_handler : app_handler_values) {
+ app_handler->OnStoreReset();
+ // app_handler might now have been deleted.
+ }
+}
+
+void GCMDriverDesktop::OnActivityRecorded(
+ const GCMClient::GCMStatistics& stats) {
+ DCHECK(ui_thread_->RunsTasksInCurrentSequence());
+
+ if (gcm_statistics_recording_callback_)
+ gcm_statistics_recording_callback_.Run(stats);
+}
+
+bool GCMDriverDesktop::TokenTupleComparer::operator()(
+ const TokenTuple& a, const TokenTuple& b) const {
+ if (std::get<0>(a) < std::get<0>(b))
+ return true;
+ if (std::get<0>(a) > std::get<0>(b))
+ return false;
+
+ if (std::get<1>(a) < std::get<1>(b))
+ return true;
+ if (std::get<1>(a) > std::get<1>(b))
+ return false;
+
+ return std::get<2>(a) < std::get<2>(b);
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_driver_desktop.h b/chromium/components/gcm_driver/gcm_driver_desktop.h
new file mode 100644
index 00000000000..fc4e5005e11
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_driver_desktop.h
@@ -0,0 +1,259 @@
+// Copyright 2014 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 COMPONENTS_GCM_DRIVER_GCM_DRIVER_DESKTOP_H_
+#define COMPONENTS_GCM_DRIVER_GCM_DRIVER_DESKTOP_H_
+
+#include <map>
+#include <memory>
+#include <string>
+#include <tuple>
+#include <vector>
+
+#include "base/compiler_specific.h"
+#include "base/containers/queue.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "base/observer_list.h"
+#include "base/tuple.h"
+#include "components/gcm_driver/crypto/gcm_decryption_result.h"
+#include "components/gcm_driver/gcm_client.h"
+#include "components/gcm_driver/gcm_connection_observer.h"
+#include "components/gcm_driver/gcm_driver.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "services/network/public/mojom/proxy_resolving_socket.mojom.h"
+
+class PrefService;
+
+namespace base {
+class FilePath;
+class SequencedTaskRunner;
+}
+
+namespace network {
+class NetworkConnectionTracker;
+class SharedURLLoaderFactory;
+}
+
+namespace gcm {
+
+class GCMAccountMapper;
+class GCMAppHandler;
+class GCMClientFactory;
+enum class GCMDecryptionResult;
+class GCMDelayedTaskController;
+
+// GCMDriver implementation for desktop and Chrome OS, using GCMClient.
+class GCMDriverDesktop : public GCMDriver,
+ protected InstanceIDHandler {
+ public:
+ // |remove_account_mappings_with_email_key| indicates whether account mappings
+ // having email as account key should be removed while loading. This is
+ // required during the migration of account identifier from email to Gaia ID.
+ GCMDriverDesktop(
+ std::unique_ptr<GCMClientFactory> gcm_client_factory,
+ const GCMClient::ChromeBuildInfo& chrome_build_info,
+ PrefService* prefs,
+ const base::FilePath& store_path,
+ bool remove_account_mappings_with_email_key,
+ base::RepeatingCallback<void(
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)>
+ get_socket_factory_callback,
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_for_ui,
+ network::NetworkConnectionTracker* network_connection_tracker,
+ const scoped_refptr<base::SequencedTaskRunner>& ui_thread,
+ const scoped_refptr<base::SequencedTaskRunner>& io_thread,
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner);
+
+ GCMDriverDesktop(const GCMDriverDesktop&) = delete;
+ GCMDriverDesktop& operator=(const GCMDriverDesktop&) = delete;
+
+ ~GCMDriverDesktop() override;
+
+ // GCMDriver implementation:
+ void ValidateRegistration(const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ const std::string& registration_id,
+ ValidateRegistrationCallback callback) override;
+ void Shutdown() override;
+ void OnSignedIn() override;
+ void OnSignedOut() override;
+ void AddAppHandler(const std::string& app_id,
+ GCMAppHandler* handler) override;
+ void RemoveAppHandler(const std::string& app_id) override;
+ void AddConnectionObserver(GCMConnectionObserver* observer) override;
+ void RemoveConnectionObserver(GCMConnectionObserver* observer) override;
+ GCMClient* GetGCMClientForTesting() const override;
+ bool IsStarted() const override;
+ bool IsConnected() const override;
+ void GetGCMStatistics(GetGCMStatisticsCallback callback,
+ ClearActivityLogs clear_logs) override;
+ void SetGCMRecording(const GCMStatisticsRecordingCallback& callback,
+ bool recording) override;
+ void SetAccountTokens(
+ const std::vector<GCMClient::AccountTokenInfo>& account_tokens) override;
+ void UpdateAccountMapping(const AccountMapping& account_mapping) override;
+ void RemoveAccountMapping(const CoreAccountId& account_id) override;
+ base::Time GetLastTokenFetchTime() override;
+ void SetLastTokenFetchTime(const base::Time& time) override;
+ InstanceIDHandler* GetInstanceIDHandlerInternal() override;
+ void AddHeartbeatInterval(const std::string& scope, int interval_ms) override;
+ void RemoveHeartbeatInterval(const std::string& scope) override;
+
+ protected:
+ // GCMDriver implementation:
+ GCMClient::Result EnsureStarted(GCMClient::StartMode start_mode) override;
+ void RegisterImpl(const std::string& app_id,
+ const std::vector<std::string>& sender_ids) override;
+ void UnregisterImpl(const std::string& app_id) override;
+ void SendImpl(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message) override;
+ void RecordDecryptionFailure(const std::string& app_id,
+ GCMDecryptionResult result) override;
+
+ // InstanceIDHandler implementation:
+ void GetToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ base::TimeDelta time_to_live,
+ GetTokenCallback callback) override;
+ void ValidateToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ const std::string& token,
+ ValidateTokenCallback callback) override;
+ void DeleteToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ DeleteTokenCallback callback) override;
+ void AddInstanceIDData(const std::string& app_id,
+ const std::string& instance_id,
+ const std::string& extra_data) override;
+ void RemoveInstanceIDData(const std::string& app_id) override;
+ void GetInstanceIDData(const std::string& app_id,
+ GetInstanceIDDataCallback callback) override;
+
+ private:
+ class IOWorker;
+
+ typedef std::tuple<std::string, std::string, std::string> TokenTuple;
+ struct TokenTupleComparer {
+ bool operator()(const TokenTuple& a, const TokenTuple& b) const;
+ };
+
+ void DoValidateRegistration(scoped_refptr<RegistrationInfo> registration_info,
+ const std::string& registration_id,
+ ValidateRegistrationCallback callback);
+
+ // Stops the GCM service. It can be restarted by calling EnsureStarted again.
+ void Stop();
+
+ // Remove cached data when GCM service is stopped.
+ void RemoveCachedData();
+
+ void DoRegister(const std::string& app_id,
+ const std::vector<std::string>& sender_ids);
+ void DoUnregister(const std::string& app_id);
+ void DoSend(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message);
+ void DoAddInstanceIDData(const std::string& app_id,
+ const std::string& instance_id,
+ const std::string& extra_data);
+ void DoRemoveInstanceIDData(const std::string& app_id);
+ void DoGetInstanceIDData(const std::string& app_id);
+ void DoGetToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ base::TimeDelta time_to_live);
+ void DoDeleteToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope);
+
+ // Callbacks posted from IO thread to UI thread.
+ void MessageReceived(const std::string& app_id,
+ const IncomingMessage& message);
+ void MessagesDeleted(const std::string& app_id);
+ void MessageSendError(const std::string& app_id,
+ const GCMClient::SendErrorDetails& send_error_details);
+ void SendAcknowledged(const std::string& app_id,
+ const std::string& message_id);
+ void GCMClientReady(const std::vector<AccountMapping>& account_mappings,
+ const base::Time& last_token_fetch_time);
+ void OnConnected(const net::IPEndPoint& ip_endpoint);
+ void OnDisconnected();
+ void OnStoreReset();
+ void OnActivityRecorded(const GCMClient::GCMStatistics& stats);
+
+ void GetInstanceIDDataFinished(const std::string& app_id,
+ const std::string& instance_id,
+ const std::string& extra_data);
+ void GetTokenFinished(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ const std::string& token,
+ GCMClient::Result result);
+ void DeleteTokenFinished(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ GCMClient::Result result);
+
+ // Flag to indicate whether the user is signed in to a GAIA account.
+ bool signed_in_;
+
+ // Flag to indicate if GCM is started.
+ bool gcm_started_;
+
+ // Flag to indicate the last known state of the GCM client. Because this
+ // flag lives on the UI thread, while the GCM client lives on the IO thread,
+ // it may be out of date while connection changes are happening.
+ bool connected_;
+
+ // List of observers to notify when connection state changes.
+ base::ObserverList<GCMConnectionObserver, false>::Unchecked
+ connection_observer_list_;
+
+ // Account mapper. Only works when user is signed in.
+ std::unique_ptr<GCMAccountMapper> account_mapper_;
+
+ // Time of last token fetching.
+ base::Time last_token_fetch_time_;
+
+ scoped_refptr<base::SequencedTaskRunner> ui_thread_;
+ scoped_refptr<base::SequencedTaskRunner> io_thread_;
+
+ std::unique_ptr<GCMDelayedTaskController> delayed_task_controller_;
+
+ // For all the work occurring on the IO thread. Must be destroyed on the IO
+ // thread.
+ std::unique_ptr<IOWorker> io_worker_;
+
+ // Callback for SetGCMRecording.
+ GCMStatisticsRecordingCallback gcm_statistics_recording_callback_;
+
+ // Callbacks for GetInstanceIDData. Initializing InstanceID is asynchronous,
+ // which leads to a race condition when recreating an InstanceID before such
+ // initialization has finished, causing multiple callbacks to be in flight.
+ // Expecting all InstanceID users to care for that is fragile and complicated,
+ // so allow for a queue of callbacks to be stored here instead.
+ //
+ // Note that other InstanceID callbacks don't have this concern, as they all
+ // wait for initialization of the InstanceID instance to have completed.
+ std::map<std::string, base::queue<GetInstanceIDDataCallback>>
+ get_instance_id_data_callbacks_;
+
+ // Callbacks for GetToken/DeleteToken.
+ std::map<TokenTuple, GetTokenCallback, TokenTupleComparer>
+ get_token_callbacks_;
+ std::map<TokenTuple, DeleteTokenCallback, TokenTupleComparer>
+ delete_token_callbacks_;
+
+ // Used to pass a weak pointer to the IO worker.
+ base::WeakPtrFactory<GCMDriverDesktop> weak_ptr_factory_{this};
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_DRIVER_DESKTOP_H_
diff --git a/chromium/components/gcm_driver/gcm_driver_desktop_unittest.cc b/chromium/components/gcm_driver/gcm_driver_desktop_unittest.cc
new file mode 100644
index 00000000000..e761bcb7f22
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_driver_desktop_unittest.cc
@@ -0,0 +1,1139 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/gcm_driver_desktop.h"
+
+#include <stdint.h>
+
+#include <memory>
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/location.h"
+#include "base/run_loop.h"
+#include "base/strings/string_util.h"
+#include "base/task/current_thread.h"
+#include "base/test/task_environment.h"
+#include "base/test/test_simple_task_runner.h"
+#include "base/threading/thread.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "components/gcm_driver/crypto/gcm_encryption_provider.h"
+#include "components/gcm_driver/fake_gcm_app_handler.h"
+#include "components/gcm_driver/fake_gcm_client.h"
+#include "components/gcm_driver/fake_gcm_client_factory.h"
+#include "components/gcm_driver/gcm_app_handler.h"
+#include "components/gcm_driver/gcm_client_factory.h"
+#include "components/gcm_driver/gcm_connection_observer.h"
+#include "components/prefs/pref_registry_simple.h"
+#include "components/prefs/testing_pref_service.h"
+#include "net/url_request/url_request_context_getter.h"
+#include "net/url_request/url_request_test_util.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
+#include "services/network/test/test_network_connection_tracker.h"
+#include "services/network/test/test_url_loader_factory.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+
+const char kTestAppID1[] = "TestApp1";
+const char kTestAppID2[] = "TestApp2";
+const char kUserID1[] = "user1";
+const char kScope[] = "GCM";
+const char kInstanceID1[] = "IID1";
+const char kInstanceID2[] = "IID2";
+
+class FakeGCMConnectionObserver : public GCMConnectionObserver {
+ public:
+ FakeGCMConnectionObserver();
+ ~FakeGCMConnectionObserver() override;
+
+ // gcm::GCMConnectionObserver implementation:
+ void OnConnected(const net::IPEndPoint& ip_endpoint) override;
+ void OnDisconnected() override;
+
+ bool connected() const { return connected_; }
+
+ private:
+ bool connected_;
+};
+
+FakeGCMConnectionObserver::FakeGCMConnectionObserver() : connected_(false) {
+}
+
+FakeGCMConnectionObserver::~FakeGCMConnectionObserver() {
+}
+
+void FakeGCMConnectionObserver::OnConnected(
+ const net::IPEndPoint& ip_endpoint) {
+ connected_ = true;
+}
+
+void FakeGCMConnectionObserver::OnDisconnected() {
+ connected_ = false;
+}
+
+void PumpCurrentLoop() {
+ base::RunLoop(base::RunLoop::Type::kNestableTasksAllowed).RunUntilIdle();
+}
+
+void PumpUILoop() {
+ PumpCurrentLoop();
+}
+
+std::vector<std::string> ToSenderList(const std::string& sender_ids) {
+ return base::SplitString(
+ sender_ids, ",", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
+}
+
+} // namespace
+
+class GCMDriverTest : public testing::Test {
+ public:
+ enum WaitToFinish {
+ DO_NOT_WAIT,
+ WAIT
+ };
+
+ GCMDriverTest();
+
+ GCMDriverTest(const GCMDriverTest&) = delete;
+ GCMDriverTest& operator=(const GCMDriverTest&) = delete;
+
+ ~GCMDriverTest() override;
+
+ // testing::Test:
+ void SetUp() override;
+ void TearDown() override;
+
+ GCMDriverDesktop* driver() { return driver_.get(); }
+ FakeGCMAppHandler* gcm_app_handler() { return gcm_app_handler_.get(); }
+ FakeGCMConnectionObserver* gcm_connection_observer() {
+ return gcm_connection_observer_.get();
+ }
+ const std::string& registration_id() const { return registration_id_; }
+ GCMClient::Result registration_result() const { return registration_result_; }
+ const std::string& send_message_id() const { return send_message_id_; }
+ GCMClient::Result send_result() const { return send_result_; }
+ GCMClient::Result unregistration_result() const {
+ return unregistration_result_;
+ }
+ const std::string& p256dh() const { return p256dh_; }
+ const std::string& auth_secret() const { return auth_secret_; }
+
+ void PumpIOLoop();
+
+ void ClearResults();
+
+ bool HasAppHandlers() const;
+ FakeGCMClient* GetGCMClient();
+
+ void CreateDriver();
+ void ShutdownDriver();
+ void AddAppHandlers();
+ void RemoveAppHandlers();
+
+ void Register(const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ WaitToFinish wait_to_finish);
+ void Send(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message,
+ WaitToFinish wait_to_finish);
+ void GetEncryptionInfo(const std::string& app_id,
+ WaitToFinish wait_to_finish);
+ void Unregister(const std::string& app_id, WaitToFinish wait_to_finish);
+
+ void WaitForAsyncOperation();
+
+ void RegisterCompleted(const std::string& registration_id,
+ GCMClient::Result result);
+ void SendCompleted(const std::string& message_id, GCMClient::Result result);
+ void GetEncryptionInfoCompleted(std::string p256dh, std::string auth_secret);
+ void UnregisterCompleted(GCMClient::Result result);
+
+ void AsyncOperationCompleted() {
+ if (async_operation_completed_callback_)
+ std::move(async_operation_completed_callback_).Run();
+ }
+ void set_async_operation_completed_callback(base::OnceClosure callback) {
+ async_operation_completed_callback_ = std::move(callback);
+ }
+
+ private:
+ base::ScopedTempDir temp_dir_;
+ TestingPrefServiceSimple prefs_;
+ base::test::SingleThreadTaskEnvironment task_environment_{
+ base::test::SingleThreadTaskEnvironment::MainThreadType::UI};
+ base::Thread io_thread_;
+ network::TestURLLoaderFactory test_url_loader_factory_;
+
+ std::unique_ptr<GCMDriverDesktop> driver_;
+ std::unique_ptr<FakeGCMAppHandler> gcm_app_handler_;
+ std::unique_ptr<FakeGCMConnectionObserver> gcm_connection_observer_;
+
+ base::OnceClosure async_operation_completed_callback_;
+
+ std::string registration_id_;
+ GCMClient::Result registration_result_;
+ std::string send_message_id_;
+ GCMClient::Result send_result_;
+ GCMClient::Result unregistration_result_;
+ std::string p256dh_;
+ std::string auth_secret_;
+};
+
+GCMDriverTest::GCMDriverTest()
+ : io_thread_("IOThread"),
+ registration_result_(GCMClient::UNKNOWN_ERROR),
+ send_result_(GCMClient::UNKNOWN_ERROR),
+ unregistration_result_(GCMClient::UNKNOWN_ERROR) {}
+
+GCMDriverTest::~GCMDriverTest() {
+}
+
+void GCMDriverTest::SetUp() {
+ io_thread_.Start();
+ ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+}
+
+void GCMDriverTest::TearDown() {
+ if (!driver_)
+ return;
+
+ ShutdownDriver();
+ driver_.reset();
+ PumpIOLoop();
+
+ io_thread_.Stop();
+ task_environment_.RunUntilIdle();
+ ASSERT_TRUE(temp_dir_.Delete());
+}
+
+void GCMDriverTest::PumpIOLoop() {
+ base::RunLoop run_loop;
+ io_thread_.task_runner()->PostTaskAndReply(
+ FROM_HERE, base::BindOnce(&PumpCurrentLoop), run_loop.QuitClosure());
+ run_loop.Run();
+}
+
+void GCMDriverTest::ClearResults() {
+ registration_id_.clear();
+ registration_result_ = GCMClient::UNKNOWN_ERROR;
+
+ send_message_id_.clear();
+ send_result_ = GCMClient::UNKNOWN_ERROR;
+
+ unregistration_result_ = GCMClient::UNKNOWN_ERROR;
+}
+
+bool GCMDriverTest::HasAppHandlers() const {
+ return !driver_->app_handlers().empty();
+}
+
+FakeGCMClient* GCMDriverTest::GetGCMClient() {
+ return static_cast<FakeGCMClient*>(driver_->GetGCMClientForTesting());
+}
+
+void GCMDriverTest::CreateDriver() {
+ scoped_refptr<net::URLRequestContextGetter> request_context =
+ new net::TestURLRequestContextGetter(io_thread_.task_runner());
+ GCMClient::ChromeBuildInfo chrome_build_info;
+ chrome_build_info.product_category_for_subtypes = "com.chrome.macosx";
+ driver_ = std::make_unique<GCMDriverDesktop>(
+ std::unique_ptr<GCMClientFactory>(new FakeGCMClientFactory(
+ base::ThreadTaskRunnerHandle::Get(), io_thread_.task_runner())),
+ chrome_build_info, &prefs_, temp_dir_.GetPath(),
+ /*remove_account_mappings_with_email_key=*/true, base::DoNothing(),
+ base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
+ &test_url_loader_factory_),
+ network::TestNetworkConnectionTracker::GetInstance(),
+ base::ThreadTaskRunnerHandle::Get(), io_thread_.task_runner(),
+ task_environment_.GetMainThreadTaskRunner());
+
+ gcm_app_handler_ = std::make_unique<FakeGCMAppHandler>();
+ gcm_connection_observer_ = std::make_unique<FakeGCMConnectionObserver>();
+
+ driver_->AddConnectionObserver(gcm_connection_observer_.get());
+}
+
+void GCMDriverTest::ShutdownDriver() {
+ if (gcm_connection_observer())
+ driver()->RemoveConnectionObserver(gcm_connection_observer());
+ driver()->Shutdown();
+}
+
+void GCMDriverTest::AddAppHandlers() {
+ driver_->AddAppHandler(kTestAppID1, gcm_app_handler_.get());
+ driver_->AddAppHandler(kTestAppID2, gcm_app_handler_.get());
+}
+
+void GCMDriverTest::RemoveAppHandlers() {
+ driver_->RemoveAppHandler(kTestAppID1);
+ driver_->RemoveAppHandler(kTestAppID2);
+}
+
+void GCMDriverTest::Register(const std::string& app_id,
+ const std::vector<std::string>& sender_ids,
+ WaitToFinish wait_to_finish) {
+ base::RunLoop run_loop;
+ async_operation_completed_callback_ = run_loop.QuitClosure();
+ driver_->Register(app_id, sender_ids,
+ base::BindOnce(&GCMDriverTest::RegisterCompleted,
+ base::Unretained(this)));
+ if (wait_to_finish == WAIT)
+ run_loop.Run();
+}
+
+void GCMDriverTest::Send(const std::string& app_id,
+ const std::string& receiver_id,
+ const OutgoingMessage& message,
+ WaitToFinish wait_to_finish) {
+ base::RunLoop run_loop;
+ async_operation_completed_callback_ = run_loop.QuitClosure();
+ driver_->Send(
+ app_id, receiver_id, message,
+ base::BindOnce(&GCMDriverTest::SendCompleted, base::Unretained(this)));
+ if (wait_to_finish == WAIT)
+ run_loop.Run();
+}
+
+void GCMDriverTest::GetEncryptionInfo(const std::string& app_id,
+ WaitToFinish wait_to_finish) {
+ base::RunLoop run_loop;
+ async_operation_completed_callback_ = run_loop.QuitClosure();
+ driver_->GetEncryptionInfo(
+ app_id, base::BindOnce(&GCMDriverTest::GetEncryptionInfoCompleted,
+ base::Unretained(this)));
+ if (wait_to_finish == WAIT)
+ run_loop.Run();
+}
+
+void GCMDriverTest::Unregister(const std::string& app_id,
+ WaitToFinish wait_to_finish) {
+ base::RunLoop run_loop;
+ async_operation_completed_callback_ = run_loop.QuitClosure();
+ driver_->Unregister(app_id,
+ base::BindOnce(&GCMDriverTest::UnregisterCompleted,
+ base::Unretained(this)));
+ if (wait_to_finish == WAIT)
+ run_loop.Run();
+}
+
+void GCMDriverTest::WaitForAsyncOperation() {
+ base::RunLoop run_loop;
+ async_operation_completed_callback_ = run_loop.QuitClosure();
+ run_loop.Run();
+}
+
+void GCMDriverTest::RegisterCompleted(const std::string& registration_id,
+ GCMClient::Result result) {
+ registration_id_ = registration_id;
+ registration_result_ = result;
+ AsyncOperationCompleted();
+}
+
+void GCMDriverTest::SendCompleted(const std::string& message_id,
+ GCMClient::Result result) {
+ send_message_id_ = message_id;
+ send_result_ = result;
+ AsyncOperationCompleted();
+}
+
+void GCMDriverTest::GetEncryptionInfoCompleted(std::string p256dh,
+ std::string auth_secret) {
+ p256dh_ = std::move(p256dh);
+ auth_secret_ = std::move(auth_secret);
+ AsyncOperationCompleted();
+}
+
+void GCMDriverTest::UnregisterCompleted(GCMClient::Result result) {
+ unregistration_result_ = result;
+ AsyncOperationCompleted();
+}
+
+TEST_F(GCMDriverTest, Create) {
+ // Create GCMDriver first. By default GCM is set to delay start.
+ CreateDriver();
+ EXPECT_FALSE(driver()->IsStarted());
+
+ // Adding an app handler will not start GCM.
+ AddAppHandlers();
+ PumpIOLoop();
+ PumpUILoop();
+ EXPECT_FALSE(driver()->IsStarted());
+ EXPECT_FALSE(driver()->IsConnected());
+ EXPECT_FALSE(gcm_connection_observer()->connected());
+
+ // The GCM registration will kick off the GCM.
+ Register(kTestAppID1, ToSenderList("sender"), GCMDriverTest::WAIT);
+ EXPECT_TRUE(driver()->IsStarted());
+ EXPECT_TRUE(driver()->IsConnected());
+ EXPECT_TRUE(gcm_connection_observer()->connected());
+}
+
+TEST_F(GCMDriverTest, Shutdown) {
+ CreateDriver();
+ EXPECT_FALSE(HasAppHandlers());
+
+ AddAppHandlers();
+ EXPECT_TRUE(HasAppHandlers());
+
+ ShutdownDriver();
+ EXPECT_FALSE(HasAppHandlers());
+ EXPECT_FALSE(driver()->IsConnected());
+ EXPECT_FALSE(gcm_connection_observer()->connected());
+}
+
+TEST_F(GCMDriverTest, StartOrStopGCMOnDemand) {
+ CreateDriver();
+ PumpIOLoop();
+ PumpUILoop();
+ EXPECT_FALSE(driver()->IsStarted());
+
+ // Adding an app handler will not start GCM.
+ driver()->AddAppHandler(kTestAppID1, gcm_app_handler());
+ PumpIOLoop();
+ PumpUILoop();
+ EXPECT_FALSE(driver()->IsStarted());
+
+ // The GCM registration will kick off the GCM.
+ Register(kTestAppID1, ToSenderList("sender"), GCMDriverTest::WAIT);
+ EXPECT_TRUE(driver()->IsStarted());
+
+ // Add another app handler.
+ driver()->AddAppHandler(kTestAppID2, gcm_app_handler());
+ PumpIOLoop();
+ PumpUILoop();
+ EXPECT_TRUE(driver()->IsStarted());
+
+ // GCMClient remains active after one app handler is gone.
+ driver()->RemoveAppHandler(kTestAppID1);
+ PumpIOLoop();
+ PumpUILoop();
+ EXPECT_TRUE(driver()->IsStarted());
+
+ // GCMClient should be stopped after the last app handler is gone.
+ driver()->RemoveAppHandler(kTestAppID2);
+ PumpIOLoop();
+ PumpUILoop();
+ EXPECT_FALSE(driver()->IsStarted());
+
+ // GCMClient is restarted after an app handler has been added.
+ driver()->AddAppHandler(kTestAppID2, gcm_app_handler());
+ PumpIOLoop();
+ PumpUILoop();
+ EXPECT_TRUE(driver()->IsStarted());
+}
+
+TEST_F(GCMDriverTest, RegisterFailed) {
+ std::vector<std::string> sender_ids;
+ sender_ids.push_back("sender1");
+
+ CreateDriver();
+
+ // Registration fails when the no app handler is added.
+ Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT);
+ EXPECT_TRUE(registration_id().empty());
+ EXPECT_EQ(GCMClient::UNKNOWN_ERROR, registration_result());
+}
+
+TEST_F(GCMDriverTest, UnregisterFailed) {
+ CreateDriver();
+
+ // Unregistration fails when the no app handler is added.
+ Unregister(kTestAppID1, GCMDriverTest::WAIT);
+ EXPECT_EQ(GCMClient::UNKNOWN_ERROR, unregistration_result());
+}
+
+TEST_F(GCMDriverTest, SendFailed) {
+ OutgoingMessage message;
+ message.id = "1";
+ message.data["key1"] = "value1";
+
+ CreateDriver();
+
+ // Sending fails when the no app handler is added.
+ Send(kTestAppID1, kUserID1, message, GCMDriverTest::WAIT);
+ EXPECT_TRUE(send_message_id().empty());
+ EXPECT_EQ(GCMClient::UNKNOWN_ERROR, send_result());
+}
+
+TEST_F(GCMDriverTest, DISABLED_GCMClientNotReadyBeforeRegistration) {
+ CreateDriver();
+ PumpIOLoop();
+ PumpUILoop();
+
+ // Make GCMClient not ready until PerformDelayedStart is called.
+ GetGCMClient()->set_start_mode_overridding(
+ FakeGCMClient::FORCE_TO_ALWAYS_DELAY_START_GCM);
+
+ AddAppHandlers();
+
+ // The registration is on hold until GCMClient is ready.
+ std::vector<std::string> sender_ids;
+ sender_ids.push_back("sender1");
+ Register(kTestAppID1,
+ sender_ids,
+ GCMDriverTest::DO_NOT_WAIT);
+ PumpIOLoop();
+ PumpUILoop();
+ EXPECT_TRUE(registration_id().empty());
+ EXPECT_EQ(GCMClient::UNKNOWN_ERROR, registration_result());
+
+ // Register operation will be invoked after GCMClient becomes ready.
+ GetGCMClient()->PerformDelayedStart();
+ WaitForAsyncOperation();
+ EXPECT_FALSE(registration_id().empty());
+ EXPECT_EQ(GCMClient::SUCCESS, registration_result());
+}
+
+TEST_F(GCMDriverTest, GCMClientNotReadyBeforeSending) {
+ CreateDriver();
+ PumpIOLoop();
+ PumpUILoop();
+
+ // Make GCMClient not ready until PerformDelayedStart is called.
+ GetGCMClient()->set_start_mode_overridding(
+ FakeGCMClient::FORCE_TO_ALWAYS_DELAY_START_GCM);
+
+ AddAppHandlers();
+
+ // The sending is on hold until GCMClient is ready.
+ OutgoingMessage message;
+ message.id = "1";
+ message.data["key1"] = "value1";
+ message.data["key2"] = "value2";
+ Send(kTestAppID1, kUserID1, message, GCMDriverTest::DO_NOT_WAIT);
+ PumpIOLoop();
+ PumpUILoop();
+
+ EXPECT_TRUE(send_message_id().empty());
+ EXPECT_EQ(GCMClient::UNKNOWN_ERROR, send_result());
+
+ // Send operation will be invoked after GCMClient becomes ready.
+ GetGCMClient()->PerformDelayedStart();
+ WaitForAsyncOperation();
+ EXPECT_EQ(message.id, send_message_id());
+ EXPECT_EQ(GCMClient::SUCCESS, send_result());
+}
+
+// Tests a single instance of GCMDriver.
+class GCMDriverFunctionalTest : public GCMDriverTest {
+ public:
+ GCMDriverFunctionalTest();
+
+ GCMDriverFunctionalTest(const GCMDriverFunctionalTest&) = delete;
+ GCMDriverFunctionalTest& operator=(const GCMDriverFunctionalTest&) = delete;
+
+ ~GCMDriverFunctionalTest() override;
+
+ // GCMDriverTest:
+ void SetUp() override;
+};
+
+GCMDriverFunctionalTest::GCMDriverFunctionalTest() {
+}
+
+GCMDriverFunctionalTest::~GCMDriverFunctionalTest() {
+}
+
+void GCMDriverFunctionalTest::SetUp() {
+ GCMDriverTest::SetUp();
+
+ CreateDriver();
+ AddAppHandlers();
+ PumpIOLoop();
+ PumpUILoop();
+}
+
+TEST_F(GCMDriverFunctionalTest, DISABLED_Register) {
+ std::vector<std::string> sender_ids;
+ sender_ids.push_back("sender1");
+ Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT);
+ const std::string expected_registration_id =
+ FakeGCMClient::GenerateGCMRegistrationID(sender_ids);
+
+ EXPECT_EQ(expected_registration_id, registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, registration_result());
+}
+
+// This test is flaky, see https://crbug.com/1010462
+TEST_F(GCMDriverFunctionalTest, DISABLED_RegisterError) {
+ std::vector<std::string> sender_ids;
+ sender_ids.push_back("sender1@error");
+ Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT);
+
+ EXPECT_TRUE(registration_id().empty());
+ EXPECT_NE(GCMClient::SUCCESS, registration_result());
+}
+
+// This test is flaky, see https://crbug.com/1010462
+TEST_F(GCMDriverFunctionalTest, DISABLED_RegisterAgainWithSameSenderIDs) {
+ std::vector<std::string> sender_ids;
+ sender_ids.push_back("sender1");
+ sender_ids.push_back("sender2");
+ Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT);
+ const std::string expected_registration_id =
+ FakeGCMClient::GenerateGCMRegistrationID(sender_ids);
+
+ EXPECT_EQ(expected_registration_id, registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, registration_result());
+
+ // Clears the results the would be set by the Register callback in preparation
+ // to call register 2nd time.
+ ClearResults();
+
+ // Calling register 2nd time with the same set of sender IDs but different
+ // ordering will get back the same registration ID.
+ std::vector<std::string> another_sender_ids;
+ another_sender_ids.push_back("sender2");
+ another_sender_ids.push_back("sender1");
+ Register(kTestAppID1, another_sender_ids, GCMDriverTest::WAIT);
+
+ EXPECT_EQ(expected_registration_id, registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, registration_result());
+}
+
+// This test is flaky, see https://crbug.com/1010462
+TEST_F(GCMDriverFunctionalTest, DISABLED_RegisterAgainWithDifferentSenderIDs) {
+ std::vector<std::string> sender_ids;
+ sender_ids.push_back("sender1");
+ Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT);
+ const std::string expected_registration_id =
+ FakeGCMClient::GenerateGCMRegistrationID(sender_ids);
+
+ EXPECT_EQ(expected_registration_id, registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, registration_result());
+
+ // Make sender IDs different.
+ sender_ids.push_back("sender2");
+ const std::string expected_registration_id2 =
+ FakeGCMClient::GenerateGCMRegistrationID(sender_ids);
+
+ // Calling register 2nd time with the different sender IDs will get back a new
+ // registration ID.
+ Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT);
+ EXPECT_EQ(expected_registration_id2, registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, registration_result());
+}
+
+TEST_F(GCMDriverFunctionalTest, UnregisterExplicitly) {
+ std::vector<std::string> sender_ids;
+ sender_ids.push_back("sender1");
+ Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT);
+
+ EXPECT_FALSE(registration_id().empty());
+ EXPECT_EQ(GCMClient::SUCCESS, registration_result());
+
+ Unregister(kTestAppID1, GCMDriverTest::WAIT);
+
+ EXPECT_EQ(GCMClient::SUCCESS, unregistration_result());
+}
+
+// TODO(crbug.com/1009185): Test is failing on ASan build.
+#if defined(ADDRESS_SANITIZER)
+TEST_F(GCMDriverFunctionalTest, DISABLED_UnregisterRemovesEncryptionInfo) {
+#else
+TEST_F(GCMDriverFunctionalTest, UnregisterRemovesEncryptionInfo) {
+#endif
+ std::vector<std::string> sender_ids;
+ sender_ids.push_back("sender1");
+ Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT);
+
+ EXPECT_FALSE(registration_id().empty());
+ EXPECT_EQ(GCMClient::SUCCESS, registration_result());
+
+ GetEncryptionInfo(kTestAppID1, GCMDriverTest::WAIT);
+
+ EXPECT_FALSE(p256dh().empty());
+ EXPECT_FALSE(auth_secret().empty());
+
+ const std::string app_p256dh = p256dh();
+ const std::string app_auth_secret = auth_secret();
+
+ GetEncryptionInfo(kTestAppID1, GCMDriverTest::WAIT);
+
+ EXPECT_EQ(app_p256dh, p256dh());
+ EXPECT_EQ(app_auth_secret, auth_secret());
+
+ Unregister(kTestAppID1, GCMDriverTest::WAIT);
+
+ EXPECT_EQ(GCMClient::SUCCESS, unregistration_result());
+
+ GetEncryptionInfo(kTestAppID1, GCMDriverTest::WAIT);
+
+ // The GCMKeyStore eagerly creates new keying material for registrations that
+ // don't have any associated with them, so the most appropriate check to do is
+ // to verify that the returned material is different from before.
+
+ EXPECT_NE(app_p256dh, p256dh());
+ EXPECT_NE(app_auth_secret, auth_secret());
+}
+
+TEST_F(GCMDriverFunctionalTest, DISABLED_UnregisterWhenAsyncOperationPending) {
+ std::vector<std::string> sender_ids;
+ sender_ids.push_back("sender1");
+ // First start registration without waiting for it to complete.
+ Register(kTestAppID1, sender_ids, GCMDriverTest::DO_NOT_WAIT);
+
+ // Test that unregistration fails with async operation pending when there is a
+ // registration already in progress.
+ Unregister(kTestAppID1, GCMDriverTest::WAIT);
+ EXPECT_EQ(GCMClient::ASYNC_OPERATION_PENDING,
+ unregistration_result());
+
+ // Complete the unregistration.
+ WaitForAsyncOperation();
+ EXPECT_EQ(GCMClient::SUCCESS, registration_result());
+
+ // Start unregistration without waiting for it to complete. This time no async
+ // operation is pending.
+ Unregister(kTestAppID1, GCMDriverTest::DO_NOT_WAIT);
+
+ // Test that unregistration fails with async operation pending when there is
+ // an unregistration already in progress.
+ Unregister(kTestAppID1, GCMDriverTest::WAIT);
+ EXPECT_EQ(GCMClient::ASYNC_OPERATION_PENDING,
+ unregistration_result());
+ ClearResults();
+
+ // Complete unregistration.
+ WaitForAsyncOperation();
+ EXPECT_EQ(GCMClient::SUCCESS, unregistration_result());
+}
+
+TEST_F(GCMDriverFunctionalTest, RegisterWhenAsyncOperationPending) {
+ std::vector<std::string> sender_ids;
+ sender_ids.push_back("sender1");
+ // First start registration without waiting for it to complete.
+ Register(kTestAppID1, sender_ids, GCMDriverTest::DO_NOT_WAIT);
+
+ // Test that registration fails with async operation pending when there is a
+ // registration already in progress.
+ Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT);
+ EXPECT_EQ(GCMClient::ASYNC_OPERATION_PENDING,
+ registration_result());
+ ClearResults();
+
+ // Complete the registration.
+ WaitForAsyncOperation();
+ EXPECT_EQ(GCMClient::SUCCESS, registration_result());
+}
+
+// This test is flaky, see https://crbug.com/1010462
+TEST_F(GCMDriverFunctionalTest, DISABLED_RegisterAfterUnfinishedUnregister) {
+ // Register and wait for it to complete.
+ std::vector<std::string> sender_ids;
+ sender_ids.push_back("sender1");
+ Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT);
+ EXPECT_EQ(GCMClient::SUCCESS, registration_result());
+ EXPECT_EQ(FakeGCMClient::GenerateGCMRegistrationID(sender_ids),
+ registration_id());
+
+ // Clears the results the would be set by the Register callback in preparation
+ // to call register 2nd time.
+ ClearResults();
+
+ // Start unregistration without waiting for it to complete.
+ Unregister(kTestAppID1, GCMDriverTest::DO_NOT_WAIT);
+
+ // Register immediately after unregistration is not completed.
+ sender_ids.push_back("sender2");
+ Register(kTestAppID1, sender_ids, GCMDriverTest::WAIT);
+
+ // We need one more waiting since the waiting in Register is indeed for
+ // uncompleted Unregister.
+ WaitForAsyncOperation();
+ EXPECT_EQ(GCMClient::SUCCESS, registration_result());
+ EXPECT_EQ(FakeGCMClient::GenerateGCMRegistrationID(sender_ids),
+ registration_id());
+}
+
+TEST_F(GCMDriverFunctionalTest, Send) {
+ OutgoingMessage message;
+ message.id = "1@ack";
+ message.data["key1"] = "value1";
+ message.data["key2"] = "value2";
+ Send(kTestAppID1, kUserID1, message, GCMDriverTest::WAIT);
+
+ EXPECT_EQ(message.id, send_message_id());
+ EXPECT_EQ(GCMClient::SUCCESS, send_result());
+
+ gcm_app_handler()->WaitForNotification();
+ EXPECT_EQ(message.id, gcm_app_handler()->acked_message_id());
+ EXPECT_EQ(kTestAppID1, gcm_app_handler()->app_id());
+}
+
+TEST_F(GCMDriverFunctionalTest, SendError) {
+ OutgoingMessage message;
+ // Embedding error in id will tell the mock to simulate the send error.
+ message.id = "1@error";
+ message.data["key1"] = "value1";
+ message.data["key2"] = "value2";
+ Send(kTestAppID1, kUserID1, message, GCMDriverTest::WAIT);
+
+ EXPECT_EQ(message.id, send_message_id());
+ EXPECT_EQ(GCMClient::SUCCESS, send_result());
+
+ // Wait for the send error.
+ gcm_app_handler()->WaitForNotification();
+ EXPECT_EQ(FakeGCMAppHandler::SEND_ERROR_EVENT,
+ gcm_app_handler()->received_event());
+ EXPECT_EQ(kTestAppID1, gcm_app_handler()->app_id());
+ EXPECT_EQ(message.id,
+ gcm_app_handler()->send_error_details().message_id);
+ EXPECT_NE(GCMClient::SUCCESS,
+ gcm_app_handler()->send_error_details().result);
+ EXPECT_EQ(message.data,
+ gcm_app_handler()->send_error_details().additional_data);
+}
+
+// This test is flaky, see https://crbug.com/1010462
+TEST_F(GCMDriverFunctionalTest, DISABLED_MessageReceived) {
+ // GCM registration has to be performed otherwise GCM will not be started.
+ Register(kTestAppID1, ToSenderList("sender"), GCMDriverTest::WAIT);
+
+ IncomingMessage message;
+ message.data["key1"] = "value1";
+ message.data["key2"] = "value2";
+ message.sender_id = "sender";
+ GetGCMClient()->ReceiveMessage(kTestAppID1, message);
+ gcm_app_handler()->WaitForNotification();
+ EXPECT_EQ(FakeGCMAppHandler::MESSAGE_EVENT,
+ gcm_app_handler()->received_event());
+ EXPECT_EQ(kTestAppID1, gcm_app_handler()->app_id());
+ EXPECT_EQ(message.data, gcm_app_handler()->message().data);
+ EXPECT_TRUE(gcm_app_handler()->message().collapse_key.empty());
+ EXPECT_EQ(message.sender_id, gcm_app_handler()->message().sender_id);
+}
+
+// This test is flaky, see https://crbug.com/1010462
+TEST_F(GCMDriverFunctionalTest, DISABLED_MessageWithCollapseKeyReceived) {
+ // GCM registration has to be performed otherwise GCM will not be started.
+ Register(kTestAppID1, ToSenderList("sender"), GCMDriverTest::WAIT);
+
+ IncomingMessage message;
+ message.data["key1"] = "value1";
+ message.collapse_key = "collapse_key_value";
+ message.sender_id = "sender";
+ GetGCMClient()->ReceiveMessage(kTestAppID1, message);
+ gcm_app_handler()->WaitForNotification();
+ EXPECT_EQ(FakeGCMAppHandler::MESSAGE_EVENT,
+ gcm_app_handler()->received_event());
+ EXPECT_EQ(kTestAppID1, gcm_app_handler()->app_id());
+ EXPECT_EQ(message.data, gcm_app_handler()->message().data);
+ EXPECT_EQ(message.collapse_key,
+ gcm_app_handler()->message().collapse_key);
+}
+
+TEST_F(GCMDriverFunctionalTest, EncryptedMessageReceivedError) {
+ // GCM registration has to be performed otherwise GCM will not be started.
+ Register(kTestAppID1, ToSenderList("sender"), GCMDriverTest::WAIT);
+
+ IncomingMessage message;
+
+ // All required information to trigger the encryption path, but with an
+ // invalid Crypto-Key header value to trigger an error.
+ message.data["encryption"] = "salt=ysyxqlYTgE0WvcZrmHbUbg";
+ message.data["crypto-key"] = "hey=thereisnopublickey";
+ message.sender_id = "sender";
+ message.raw_data = "foobar";
+
+ GetGCMClient()->SetRecording(true);
+ GetGCMClient()->ReceiveMessage(kTestAppID1, message);
+
+ PumpIOLoop();
+ PumpUILoop();
+ PumpIOLoop();
+
+ EXPECT_EQ(FakeGCMAppHandler::DECRYPTION_FAILED_EVENT,
+ gcm_app_handler()->received_event());
+
+ GCMClient::GCMStatistics statistics = GetGCMClient()->GetStatistics();
+ EXPECT_TRUE(statistics.is_recording);
+ EXPECT_EQ(
+ 1u, statistics.recorded_activities.decryption_failure_activities.size());
+}
+
+TEST_F(GCMDriverFunctionalTest, MessagesDeleted) {
+ // GCM registration has to be performed otherwise GCM will not be started.
+ Register(kTestAppID1, ToSenderList("sender"), GCMDriverTest::WAIT);
+
+ GetGCMClient()->DeleteMessages(kTestAppID1);
+ gcm_app_handler()->WaitForNotification();
+ EXPECT_EQ(FakeGCMAppHandler::MESSAGES_DELETED_EVENT,
+ gcm_app_handler()->received_event());
+ EXPECT_EQ(kTestAppID1, gcm_app_handler()->app_id());
+}
+
+TEST_F(GCMDriverFunctionalTest, LastTokenFetchTime) {
+ // GCM registration has to be performed otherwise GCM will not be started.
+ Register(kTestAppID1, ToSenderList("sender"), GCMDriverTest::WAIT);
+
+ EXPECT_EQ(base::Time(), driver()->GetLastTokenFetchTime());
+ base::Time fetch_time = base::Time::Now();
+ driver()->SetLastTokenFetchTime(fetch_time);
+ EXPECT_EQ(fetch_time, driver()->GetLastTokenFetchTime());
+}
+
+class GCMDriverInstanceIDTest : public GCMDriverTest {
+ public:
+ GCMDriverInstanceIDTest();
+
+ GCMDriverInstanceIDTest(const GCMDriverInstanceIDTest&) = delete;
+ GCMDriverInstanceIDTest& operator=(const GCMDriverInstanceIDTest&) = delete;
+
+ ~GCMDriverInstanceIDTest() override;
+
+ void GetReady();
+ void GetInstanceID(const std::string& app_id, WaitToFinish wait_to_finish);
+ void GetInstanceIDDataCompleted(const std::string& instance_id,
+ const std::string& extra_data);
+ void GetToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ WaitToFinish wait_to_finish);
+ void DeleteToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ WaitToFinish wait_to_finish);
+ void AddInstanceIDData(const std::string& app_id,
+ const std::string& instance_id,
+ const std::string& extra_data);
+ void RemoveInstanceIDData(const std::string& app_id);
+
+ std::string instance_id() const { return instance_id_; }
+ std::string extra_data() const { return extra_data_; }
+
+ int instance_id_resolved_counter() const {
+ return instance_id_resolved_counter_;
+ }
+
+ private:
+ std::string instance_id_;
+ std::string extra_data_;
+
+ int instance_id_resolved_counter_ = 0;
+};
+
+GCMDriverInstanceIDTest::GCMDriverInstanceIDTest() {
+}
+
+GCMDriverInstanceIDTest::~GCMDriverInstanceIDTest() {
+}
+
+void GCMDriverInstanceIDTest::GetReady() {
+ CreateDriver();
+ AddAppHandlers();
+ PumpIOLoop();
+ PumpUILoop();
+}
+
+void GCMDriverInstanceIDTest::GetInstanceID(const std::string& app_id,
+ WaitToFinish wait_to_finish) {
+ base::RunLoop run_loop;
+ set_async_operation_completed_callback(run_loop.QuitClosure());
+ driver()->GetInstanceIDHandlerInternal()->GetInstanceIDData(
+ app_id,
+ base::BindOnce(&GCMDriverInstanceIDTest::GetInstanceIDDataCompleted,
+ base::Unretained(this)));
+ if (wait_to_finish == WAIT)
+ run_loop.Run();
+}
+
+void GCMDriverInstanceIDTest::GetInstanceIDDataCompleted(
+ const std::string& instance_id, const std::string& extra_data) {
+ instance_id_ = instance_id;
+ extra_data_ = extra_data;
+
+ instance_id_resolved_counter_++;
+
+ AsyncOperationCompleted();
+}
+
+void GCMDriverInstanceIDTest::GetToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ WaitToFinish wait_to_finish) {
+ base::RunLoop run_loop;
+ set_async_operation_completed_callback(run_loop.QuitClosure());
+ driver()->GetInstanceIDHandlerInternal()->GetToken(
+ app_id, authorized_entity, scope, /*time_to_live=*/base::TimeDelta(),
+ base::BindOnce(&GCMDriverTest::RegisterCompleted,
+ base::Unretained(this)));
+ if (wait_to_finish == WAIT)
+ run_loop.Run();
+}
+
+void GCMDriverInstanceIDTest::DeleteToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ WaitToFinish wait_to_finish) {
+ base::RunLoop run_loop;
+ set_async_operation_completed_callback(run_loop.QuitClosure());
+ driver()->GetInstanceIDHandlerInternal()->DeleteToken(
+ app_id, authorized_entity, scope,
+ base::BindOnce(&GCMDriverTest::UnregisterCompleted,
+ base::Unretained(this)));
+ if (wait_to_finish == WAIT)
+ run_loop.Run();
+}
+
+void GCMDriverInstanceIDTest::AddInstanceIDData(const std::string& app_id,
+ const std::string& instance_id,
+ const std::string& extra_data) {
+ driver()->GetInstanceIDHandlerInternal()->AddInstanceIDData(
+ app_id, instance_id, extra_data);
+}
+
+void GCMDriverInstanceIDTest::RemoveInstanceIDData(const std::string& app_id) {
+ driver()->GetInstanceIDHandlerInternal()->RemoveInstanceIDData(app_id);
+}
+
+TEST_F(GCMDriverInstanceIDTest, InstanceIDData) {
+ GetReady();
+
+ AddInstanceIDData(kTestAppID1, kInstanceID1, "Foo");
+ GetInstanceID(kTestAppID1, GCMDriverTest::WAIT);
+
+ EXPECT_EQ(kInstanceID1, instance_id());
+ EXPECT_EQ("Foo", extra_data());
+ EXPECT_EQ(1, instance_id_resolved_counter());
+
+ RemoveInstanceIDData(kTestAppID1);
+ GetInstanceID(kTestAppID1, GCMDriverTest::WAIT);
+
+ EXPECT_TRUE(instance_id().empty());
+ EXPECT_TRUE(extra_data().empty());
+ EXPECT_EQ(2, instance_id_resolved_counter());
+
+ AddInstanceIDData(kTestAppID1, kInstanceID1, "Bar");
+ GetInstanceID(kTestAppID1, GCMDriverTest::DO_NOT_WAIT);
+ GetInstanceID(kTestAppID1, GCMDriverTest::DO_NOT_WAIT);
+
+ WaitForAsyncOperation();
+ WaitForAsyncOperation();
+
+ EXPECT_EQ(kInstanceID1, instance_id());
+ EXPECT_EQ("Bar", extra_data());
+ EXPECT_EQ(4, instance_id_resolved_counter());
+}
+
+// This test is flaky, see https://crbug.com/1010462
+TEST_F(GCMDriverInstanceIDTest,
+ DISABLED_GCMClientNotReadyBeforeInstanceIDData) {
+ CreateDriver();
+ PumpIOLoop();
+ PumpUILoop();
+
+ // Make GCMClient not ready until PerformDelayedStart is called.
+ GetGCMClient()->set_start_mode_overridding(
+ FakeGCMClient::FORCE_TO_ALWAYS_DELAY_START_GCM);
+
+ AddAppHandlers();
+
+ // All operations are on hold until GCMClient is ready.
+ AddInstanceIDData(kTestAppID1, kInstanceID1, "Foo");
+ AddInstanceIDData(kTestAppID2, kInstanceID2, "Bar");
+ RemoveInstanceIDData(kTestAppID1);
+ GetInstanceID(kTestAppID2, GCMDriverTest::DO_NOT_WAIT);
+ PumpIOLoop();
+ PumpUILoop();
+ EXPECT_TRUE(instance_id().empty());
+ EXPECT_TRUE(extra_data().empty());
+
+ // All operations will be performed after GCMClient becomes ready.
+ GetGCMClient()->PerformDelayedStart();
+ WaitForAsyncOperation();
+ EXPECT_EQ(kInstanceID2, instance_id());
+ EXPECT_EQ("Bar", extra_data());
+}
+
+TEST_F(GCMDriverInstanceIDTest, DISABLED_GetToken) {
+ GetReady();
+
+ const std::string expected_token =
+ FakeGCMClient::GenerateInstanceIDToken(kUserID1, kScope);
+ GetToken(kTestAppID1, kUserID1, kScope, GCMDriverTest::WAIT);
+
+ EXPECT_EQ(expected_token, registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, registration_result());
+}
+
+TEST_F(GCMDriverInstanceIDTest, GetTokenError) {
+ GetReady();
+
+ std::string error_entity = "sender@error";
+ GetToken(kTestAppID1, error_entity, kScope, GCMDriverTest::WAIT);
+
+ EXPECT_TRUE(registration_id().empty());
+ EXPECT_NE(GCMClient::SUCCESS, registration_result());
+}
+
+TEST_F(GCMDriverInstanceIDTest, GCMClientNotReadyBeforeGetToken) {
+ CreateDriver();
+ PumpIOLoop();
+ PumpUILoop();
+
+ // Make GCMClient not ready until PerformDelayedStart is called.
+ GetGCMClient()->set_start_mode_overridding(
+ FakeGCMClient::FORCE_TO_ALWAYS_DELAY_START_GCM);
+
+ AddAppHandlers();
+
+ // GetToken operation is on hold until GCMClient is ready.
+ GetToken(kTestAppID1, kUserID1, kScope, GCMDriverTest::DO_NOT_WAIT);
+ PumpIOLoop();
+ PumpUILoop();
+ EXPECT_TRUE(registration_id().empty());
+ EXPECT_EQ(GCMClient::UNKNOWN_ERROR, registration_result());
+
+ // GetToken operation will be invoked after GCMClient becomes ready.
+ GetGCMClient()->PerformDelayedStart();
+ WaitForAsyncOperation();
+ EXPECT_FALSE(registration_id().empty());
+ EXPECT_EQ(GCMClient::SUCCESS, registration_result());
+}
+
+TEST_F(GCMDriverInstanceIDTest, DeleteToken) {
+ GetReady();
+
+ const std::string expected_token =
+ FakeGCMClient::GenerateInstanceIDToken(kUserID1, kScope);
+ GetToken(kTestAppID1, kUserID1, kScope, GCMDriverTest::WAIT);
+ EXPECT_EQ(expected_token, registration_id());
+ EXPECT_EQ(GCMClient::SUCCESS, registration_result());
+
+ DeleteToken(kTestAppID1, kUserID1, kScope, GCMDriverTest::WAIT);
+ EXPECT_EQ(GCMClient::SUCCESS, unregistration_result());
+}
+
+TEST_F(GCMDriverInstanceIDTest, GCMClientNotReadyBeforeDeleteToken) {
+ CreateDriver();
+ PumpIOLoop();
+ PumpUILoop();
+
+ // Make GCMClient not ready until PerformDelayedStart is called.
+ GetGCMClient()->set_start_mode_overridding(
+ FakeGCMClient::FORCE_TO_ALWAYS_DELAY_START_GCM);
+
+ AddAppHandlers();
+
+ // DeleteToken operation is on hold until GCMClient is ready.
+ DeleteToken(kTestAppID1, kUserID1, kScope, GCMDriverTest::DO_NOT_WAIT);
+ PumpIOLoop();
+ PumpUILoop();
+ EXPECT_EQ(GCMClient::UNKNOWN_ERROR, unregistration_result());
+
+ // DeleteToken operation will be invoked after GCMClient becomes ready.
+ GetGCMClient()->PerformDelayedStart();
+ WaitForAsyncOperation();
+ EXPECT_EQ(GCMClient::SUCCESS, unregistration_result());
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_driver_unittest.cc b/chromium/components/gcm_driver/gcm_driver_unittest.cc
new file mode 100644
index 00000000000..1bba7d56a23
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_driver_unittest.cc
@@ -0,0 +1,269 @@
+// Copyright 2019 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.
+
+#include "components/gcm_driver/gcm_driver_desktop.h"
+
+#include <stdint.h>
+
+#include "base/base64.h"
+#include "base/bind.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/run_loop.h"
+#include "base/task/current_thread.h"
+#include "base/test/task_environment.h"
+#include "base/test/test_simple_task_runner.h"
+#include "base/threading/thread.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "components/gcm_driver/crypto/gcm_decryption_result.h"
+#include "components/gcm_driver/crypto/gcm_encryption_provider.h"
+#include "components/gcm_driver/crypto/gcm_encryption_result.h"
+#include "components/gcm_driver/fake_gcm_client_factory.h"
+#include "components/gcm_driver/gcm_client_factory.h"
+#include "components/prefs/pref_registry_simple.h"
+#include "components/prefs/testing_pref_service.h"
+#include "crypto/ec_private_key.h"
+#include "net/url_request/url_request_context_getter.h"
+#include "net/url_request/url_request_test_util.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
+#include "services/network/test/test_network_connection_tracker.h"
+#include "services/network/test/test_url_loader_factory.h"
+#include "services/network/test/test_utils.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+
+namespace gcm {
+
+namespace {
+
+const char kTestAppID1[] = "TestApp1";
+
+void PumpCurrentLoop() {
+ base::RunLoop(base::RunLoop::Type::kNestableTasksAllowed).RunUntilIdle();
+}
+
+} // namespace
+
+class GCMDriverBaseTest : public testing::Test {
+ public:
+ enum WaitToFinish { DO_NOT_WAIT, WAIT };
+
+ GCMDriverBaseTest();
+
+ GCMDriverBaseTest(const GCMDriverBaseTest&) = delete;
+ GCMDriverBaseTest& operator=(const GCMDriverBaseTest&) = delete;
+
+ ~GCMDriverBaseTest() override;
+
+ // testing::Test:
+ void SetUp() override;
+ void TearDown() override;
+
+ GCMDriverDesktop* driver() { return driver_.get(); }
+
+ const std::string& p256dh() const { return p256dh_; }
+ const std::string& auth_secret() const { return auth_secret_; }
+ network::TestURLLoaderFactory& loader() { return test_url_loader_factory_; }
+ GCMEncryptionResult encryption_result() { return encryption_result_; }
+ const std::string& encrypted_message() { return encrypted_message_; }
+ GCMDecryptionResult decryption_result() { return decryption_result_; }
+ const std::string& decrypted_message() { return decrypted_message_; }
+
+ void PumpIOLoop();
+
+ void CreateDriver();
+ void ShutdownDriver();
+
+ void GetEncryptionInfo(const std::string& app_id,
+ WaitToFinish wait_to_finish);
+ void EncryptMessage(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& p256dh,
+ const std::string& auth_secret,
+ const std::string& message,
+ WaitToFinish wait_to_finish);
+ void DecryptMessage(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& message,
+ WaitToFinish wait_to_finish);
+
+ void GetEncryptionInfoCompleted(std::string p256dh, std::string auth_secret);
+ void EncryptMessageCompleted(GCMEncryptionResult result, std::string message);
+ void DecryptMessageCompleted(GCMDecryptionResult result, std::string message);
+ void UnregisterCompleted(GCMClient::Result result);
+
+ private:
+ base::ScopedTempDir temp_dir_;
+ TestingPrefServiceSimple prefs_;
+ base::test::TaskEnvironment task_environment_{
+ base::test::TaskEnvironment::MainThreadType::UI};
+ base::Thread io_thread_;
+ network::TestURLLoaderFactory test_url_loader_factory_;
+
+ std::unique_ptr<GCMDriverDesktop> driver_;
+
+ base::OnceClosure async_operation_completed_callback_;
+ std::string p256dh_;
+ std::string auth_secret_;
+
+ GCMEncryptionResult encryption_result_ =
+ GCMEncryptionResult::ENCRYPTION_FAILED;
+ std::string encrypted_message_;
+ GCMDecryptionResult decryption_result_ = GCMDecryptionResult::UNENCRYPTED;
+ std::string decrypted_message_;
+};
+
+GCMDriverBaseTest::GCMDriverBaseTest() : io_thread_("IOThread") {}
+
+GCMDriverBaseTest::~GCMDriverBaseTest() = default;
+
+void GCMDriverBaseTest::SetUp() {
+ io_thread_.Start();
+ ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+
+ CreateDriver();
+ PumpIOLoop();
+ PumpCurrentLoop();
+}
+
+void GCMDriverBaseTest::TearDown() {
+ if (!driver_)
+ return;
+
+ ShutdownDriver();
+ driver_.reset();
+ PumpIOLoop();
+
+ io_thread_.Stop();
+ task_environment_.RunUntilIdle();
+ ASSERT_TRUE(temp_dir_.Delete());
+}
+
+void GCMDriverBaseTest::PumpIOLoop() {
+ base::RunLoop run_loop;
+ io_thread_.task_runner()->PostTaskAndReply(
+ FROM_HERE, base::BindOnce(&PumpCurrentLoop), run_loop.QuitClosure());
+ run_loop.Run();
+}
+
+void GCMDriverBaseTest::CreateDriver() {
+ scoped_refptr<net::URLRequestContextGetter> request_context =
+ new net::TestURLRequestContextGetter(io_thread_.task_runner());
+ GCMClient::ChromeBuildInfo chrome_build_info;
+ chrome_build_info.product_category_for_subtypes = "com.chrome.macosx";
+ driver_ = std::make_unique<GCMDriverDesktop>(
+ std::make_unique<FakeGCMClientFactory>(
+ base::ThreadTaskRunnerHandle::Get(), io_thread_.task_runner()),
+ chrome_build_info, &prefs_, temp_dir_.GetPath(),
+ /*remove_account_mappings_with_email_key=*/true, base::DoNothing(),
+ base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
+ &test_url_loader_factory_),
+ network::TestNetworkConnectionTracker::GetInstance(),
+ base::ThreadTaskRunnerHandle::Get(), io_thread_.task_runner(),
+ task_environment_.GetMainThreadTaskRunner());
+}
+
+void GCMDriverBaseTest::ShutdownDriver() {
+ driver()->Shutdown();
+}
+
+void GCMDriverBaseTest::GetEncryptionInfo(const std::string& app_id,
+ WaitToFinish wait_to_finish) {
+ base::RunLoop run_loop;
+ async_operation_completed_callback_ = run_loop.QuitClosure();
+ driver_->GetEncryptionInfo(
+ app_id, base::BindOnce(&GCMDriverBaseTest::GetEncryptionInfoCompleted,
+ base::Unretained(this)));
+ if (wait_to_finish == WAIT)
+ run_loop.Run();
+}
+
+void GCMDriverBaseTest::EncryptMessage(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& p256dh,
+ const std::string& auth_secret,
+ const std::string& message,
+ WaitToFinish wait_to_finish) {
+ base::RunLoop run_loop;
+ async_operation_completed_callback_ = run_loop.QuitClosure();
+
+ driver()->EncryptMessage(
+ app_id, authorized_entity, p256dh, auth_secret, message,
+ base::BindOnce(&GCMDriverBaseTest::EncryptMessageCompleted,
+ base::Unretained(this)));
+
+ if (wait_to_finish == WAIT)
+ run_loop.Run();
+}
+
+void GCMDriverBaseTest::DecryptMessage(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& message,
+ WaitToFinish wait_to_finish) {
+ base::RunLoop run_loop;
+ async_operation_completed_callback_ = run_loop.QuitClosure();
+
+ driver()->DecryptMessage(
+ app_id, authorized_entity, message,
+ base::BindOnce(&GCMDriverBaseTest::DecryptMessageCompleted,
+ base::Unretained(this)));
+
+ if (wait_to_finish == WAIT)
+ run_loop.Run();
+}
+
+void GCMDriverBaseTest::GetEncryptionInfoCompleted(std::string p256dh,
+ std::string auth_secret) {
+ p256dh_ = std::move(p256dh);
+ auth_secret_ = std::move(auth_secret);
+ if (!async_operation_completed_callback_.is_null())
+ std::move(async_operation_completed_callback_).Run();
+}
+
+void GCMDriverBaseTest::EncryptMessageCompleted(GCMEncryptionResult result,
+ std::string message) {
+ encryption_result_ = result;
+ encrypted_message_ = std::move(message);
+ if (!async_operation_completed_callback_.is_null())
+ std::move(async_operation_completed_callback_).Run();
+}
+
+void GCMDriverBaseTest::DecryptMessageCompleted(GCMDecryptionResult result,
+ std::string message) {
+ decryption_result_ = result;
+ decrypted_message_ = std::move(message);
+ if (!async_operation_completed_callback_.is_null())
+ std::move(async_operation_completed_callback_).Run();
+}
+
+TEST_F(GCMDriverBaseTest, EncryptionDecryptionRoundTrip) {
+ GetEncryptionInfo(kTestAppID1, GCMDriverBaseTest::WAIT);
+
+ std::string message = "payload";
+ ASSERT_NO_FATAL_FAILURE(
+ EncryptMessage(kTestAppID1, /* authorized_entity= */ "", p256dh(),
+ auth_secret(), message, GCMDriverBaseTest::WAIT));
+
+ EXPECT_EQ(GCMEncryptionResult::ENCRYPTED_DRAFT_08, encryption_result());
+
+ ASSERT_NO_FATAL_FAILURE(
+ DecryptMessage(kTestAppID1, /* authorized_entity= */ "",
+ encrypted_message(), GCMDriverBaseTest::WAIT));
+
+ EXPECT_EQ(GCMDecryptionResult::DECRYPTED_DRAFT_08, decryption_result());
+ EXPECT_EQ(message, decrypted_message());
+}
+
+TEST_F(GCMDriverBaseTest, EncryptionError) {
+ // Intentionally not creating encryption info.
+
+ std::string message = "payload";
+ ASSERT_NO_FATAL_FAILURE(
+ EncryptMessage(kTestAppID1, /* authorized_entity= */ "", p256dh(),
+ auth_secret(), message, GCMDriverBaseTest::WAIT));
+
+ EXPECT_EQ(GCMEncryptionResult::NO_KEYS, encryption_result());
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_internals_constants.cc b/chromium/components/gcm_driver/gcm_internals_constants.cc
new file mode 100644
index 00000000000..94fc32ba0b0
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_internals_constants.cc
@@ -0,0 +1,41 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/gcm_internals_constants.h"
+
+namespace gcm_driver {
+
+// Resource paths.
+const char kGcmInternalsCSS[] = "gcm_internals.css";
+const char kGcmInternalsJS[] = "gcm_internals.js";
+
+// Message handlers.
+const char kGetGcmInternalsInfo[] = "getGcmInternalsInfo";
+const char kSetGcmInternalsInfo[] = "set-gcm-internals-info";
+const char kSetGcmInternalsRecording[] = "setGcmInternalsRecording";
+
+// GCM internal info.
+const char kAndroidId[] = "androidId";
+const char kAndroidSecret[] = "androidSecret";
+const char kCheckinInfo[] = "checkinInfo";
+const char kConnectionClientCreated[] = "connectionClientCreated";
+const char kConnectionInfo[] = "connectionInfo";
+const char kConnectionState[] = "connectionState";
+const char kDeviceInfo[] = "deviceInfo";
+const char kGcmClientCreated[] = "gcmClientCreated";
+const char kGcmClientState[] = "gcmClientState";
+const char kGcmEnabled[] = "gcmEnabled";
+const char kIsRecording[] = "isRecording";
+const char kLastCheckin[] = "lastCheckin";
+const char kNextCheckin[] = "nextCheckin";
+const char kProfileServiceCreated[] = "profileServiceCreated";
+const char kReceiveInfo[] = "receiveInfo";
+const char kRegisteredAppIds[] = "registeredAppIds";
+const char kRegistrationInfo[] = "registrationInfo";
+const char kResendQueueSize[] = "resendQueueSize";
+const char kSendInfo[] = "sendInfo";
+const char kSendQueueSize[] = "sendQueueSize";
+const char kDecryptionFailureInfo[] = "decryptionFailureInfo";
+
+} // namespace gcm_driver
diff --git a/chromium/components/gcm_driver/gcm_internals_constants.h b/chromium/components/gcm_driver/gcm_internals_constants.h
new file mode 100644
index 00000000000..54623bc4303
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_internals_constants.h
@@ -0,0 +1,47 @@
+// Copyright 2015 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 COMPONENTS_GCM_DRIVER_GCM_INTERNALS_CONSTANTS_H_
+#define COMPONENTS_GCM_DRIVER_GCM_INTERNALS_CONSTANTS_H_
+
+namespace gcm_driver {
+
+// Resource paths.
+// Must match the resource file names.
+extern const char kGcmInternalsCSS[];
+extern const char kGcmInternalsJS[];
+
+// Message handlers.
+// Must match the constants used in the resource files.
+extern const char kGetGcmInternalsInfo[];
+extern const char kSetGcmInternalsInfo[];
+extern const char kSetGcmInternalsRecording[];
+
+// GCM internal info.
+// Must match the constants used in the resource files.
+extern const char kAndroidId[];
+extern const char kAndroidSecret[];
+extern const char kCheckinInfo[];
+extern const char kConnectionClientCreated[];
+extern const char kConnectionInfo[];
+extern const char kConnectionState[];
+extern const char kDeviceInfo[];
+extern const char kGcmClientCreated[];
+extern const char kGcmClientState[];
+extern const char kGcmEnabled[];
+extern const char kIsRecording[];
+extern const char kLastCheckin[];
+extern const char kNextCheckin[];
+extern const char kProfileServiceCreated[];
+extern const char kReceiveInfo[];
+extern const char kRegisteredAppIds[];
+extern const char kRegistrationInfo[];
+extern const char kResendQueueSize[];
+extern const char kSendInfo[];
+extern const char kSendQueueSize[];
+extern const char kDecryptionFailureInfo[];
+
+} // namespace gcm_driver
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_INTERNALS_CONSTANTS_H_
diff --git a/chromium/components/gcm_driver/gcm_internals_helper.cc b/chromium/components/gcm_driver/gcm_internals_helper.cc
new file mode 100644
index 00000000000..6b360d6a526
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_internals_helper.cc
@@ -0,0 +1,190 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/gcm_internals_helper.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/format_macros.h"
+#include "base/i18n/time_formatting.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/values.h"
+#include "components/gcm_driver/gcm_activity.h"
+#include "components/gcm_driver/gcm_internals_constants.h"
+#include "components/gcm_driver/gcm_profile_service.h"
+
+namespace gcm_driver {
+
+namespace {
+
+void SetCheckinInfo(const std::vector<gcm::CheckinActivity>& checkins,
+ std::vector<base::Value>* checkin_info) {
+ for (const gcm::CheckinActivity& checkin : checkins) {
+ base::Value row(base::Value::Type::LIST);
+ row.Append(checkin.time.ToJsTime());
+ row.Append(checkin.event);
+ row.Append(checkin.details);
+ checkin_info->push_back(std::move(row));
+ }
+}
+
+void SetConnectionInfo(const std::vector<gcm::ConnectionActivity>& connections,
+ std::vector<base::Value>* connection_info) {
+ for (const gcm::ConnectionActivity& connection : connections) {
+ base::Value row(base::Value::Type::LIST);
+ row.Append(connection.time.ToJsTime());
+ row.Append(connection.event);
+ row.Append(connection.details);
+ connection_info->push_back(std::move(row));
+ }
+}
+
+void SetRegistrationInfo(
+ const std::vector<gcm::RegistrationActivity>& registrations,
+ std::vector<base::Value>* registration_info) {
+ for (const gcm::RegistrationActivity& registration : registrations) {
+ base::Value row(base::Value::Type::LIST);
+ row.Append(registration.time.ToJsTime());
+ row.Append(registration.app_id);
+ row.Append(registration.source);
+ row.Append(registration.event);
+ row.Append(registration.details);
+ registration_info->push_back(std::move(row));
+ }
+}
+
+void SetReceivingInfo(const std::vector<gcm::ReceivingActivity>& receives,
+ std::vector<base::Value>* receive_info) {
+ for (const gcm::ReceivingActivity& receive : receives) {
+ base::Value row(base::Value::Type::LIST);
+ row.Append(receive.time.ToJsTime());
+ row.Append(receive.app_id);
+ row.Append(receive.from);
+ row.Append(base::NumberToString(receive.message_byte_size));
+ row.Append(receive.event);
+ row.Append(receive.details);
+ receive_info->push_back(std::move(row));
+ }
+}
+
+void SetSendingInfo(const std::vector<gcm::SendingActivity>& sends,
+ std::vector<base::Value>* send_info) {
+ for (const gcm::SendingActivity& send : sends) {
+ base::Value row(base::Value::Type::LIST);
+ row.Append(send.time.ToJsTime());
+ row.Append(send.app_id);
+ row.Append(send.receiver_id);
+ row.Append(send.message_id);
+ row.Append(send.event);
+ row.Append(send.details);
+ send_info->push_back(std::move(row));
+ }
+}
+
+void SetDecryptionFailureInfo(
+ const std::vector<gcm::DecryptionFailureActivity>& failures,
+ std::vector<base::Value>* failure_info) {
+ for (const gcm::DecryptionFailureActivity& failure : failures) {
+ base::Value row(base::Value::Type::LIST);
+ row.Append(failure.time.ToJsTime());
+ row.Append(failure.app_id);
+ row.Append(failure.details);
+ failure_info->push_back(std::move(row));
+ }
+}
+
+} // namespace
+
+void SetGCMInternalsInfo(const gcm::GCMClient::GCMStatistics* stats,
+ gcm::GCMProfileService* profile_service,
+ PrefService* prefs,
+ base::DictionaryValue* results) {
+ base::Value device_info(base::Value::Type::DICTIONARY);
+
+ device_info.SetBoolKey(kProfileServiceCreated, profile_service != nullptr);
+ device_info.SetBoolKey(kGcmEnabled, true);
+ if (stats) {
+ results->SetBoolKey(kIsRecording, stats->is_recording);
+ device_info.SetBoolKey(kGcmClientCreated, stats->gcm_client_created);
+ device_info.SetStringKey(kGcmClientState, stats->gcm_client_state);
+ device_info.SetBoolKey(kConnectionClientCreated,
+ stats->connection_client_created);
+
+ base::Value registered_app_ids(base::Value::Type::LIST);
+ for (const std::string& app_id : stats->registered_app_ids)
+ registered_app_ids.Append(app_id);
+
+ device_info.SetKey(kRegisteredAppIds, std::move(registered_app_ids));
+
+ if (stats->connection_client_created)
+ device_info.SetStringKey(kConnectionState, stats->connection_state);
+ if (!stats->last_checkin.is_null()) {
+ device_info.SetStringKey(
+ kLastCheckin, base::UTF16ToUTF8(base::TimeFormatFriendlyDateAndTime(
+ stats->last_checkin)));
+ }
+ if (!stats->next_checkin.is_null()) {
+ device_info.SetStringKey(
+ kNextCheckin, base::UTF16ToUTF8(base::TimeFormatFriendlyDateAndTime(
+ stats->next_checkin)));
+ }
+ if (stats->android_id > 0) {
+ device_info.SetStringKey(
+ kAndroidId, base::StringPrintf("0x%" PRIx64, stats->android_id));
+ }
+ if (stats->android_secret > 0) {
+ device_info.SetStringKey(kAndroidSecret,
+ base::NumberToString(stats->android_secret));
+ }
+ device_info.SetIntKey(kSendQueueSize, stats->send_queue_size);
+ device_info.SetIntKey(kResendQueueSize, stats->resend_queue_size);
+ results->SetKey(kDeviceInfo, std::move(device_info));
+
+ if (stats->recorded_activities.checkin_activities.size() > 0) {
+ std::vector<base::Value> checkin_info;
+ SetCheckinInfo(stats->recorded_activities.checkin_activities,
+ &checkin_info);
+ results->SetKey(kCheckinInfo, base::Value(std::move(checkin_info)));
+ }
+ if (stats->recorded_activities.connection_activities.size() > 0) {
+ std::vector<base::Value> connection_info;
+ SetConnectionInfo(stats->recorded_activities.connection_activities,
+ &connection_info);
+ results->SetKey(kConnectionInfo, base::Value(std::move(connection_info)));
+ }
+ if (stats->recorded_activities.registration_activities.size() > 0) {
+ std::vector<base::Value> registration_info;
+ SetRegistrationInfo(stats->recorded_activities.registration_activities,
+ &registration_info);
+ results->SetKey(kRegistrationInfo,
+ base::Value(std::move(registration_info)));
+ }
+ if (stats->recorded_activities.receiving_activities.size() > 0) {
+ std::vector<base::Value> receive_info;
+ SetReceivingInfo(stats->recorded_activities.receiving_activities,
+ &receive_info);
+ results->SetKey(kReceiveInfo, base::Value(std::move(receive_info)));
+ }
+ if (stats->recorded_activities.sending_activities.size() > 0) {
+ std::vector<base::Value> send_info;
+ SetSendingInfo(stats->recorded_activities.sending_activities, &send_info);
+ results->SetKey(kSendInfo, base::Value(std::move(send_info)));
+ }
+
+ if (stats->recorded_activities.decryption_failure_activities.size() > 0) {
+ std::vector<base::Value> failure_info;
+ SetDecryptionFailureInfo(
+ stats->recorded_activities.decryption_failure_activities,
+ &failure_info);
+ results->SetKey(kDecryptionFailureInfo,
+ base::Value(std::move(failure_info)));
+ }
+ }
+}
+
+} // namespace gcm_driver
diff --git a/chromium/components/gcm_driver/gcm_internals_helper.h b/chromium/components/gcm_driver/gcm_internals_helper.h
new file mode 100644
index 00000000000..1ea48b23b8a
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_internals_helper.h
@@ -0,0 +1,30 @@
+// Copyright 2015 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 COMPONENTS_GCM_DRIVER_GCM_INTERNALS_HELPER_H_
+#define COMPONENTS_GCM_DRIVER_GCM_INTERNALS_HELPER_H_
+
+#include "components/gcm_driver/gcm_client.h"
+
+class PrefService;
+
+namespace base {
+class DictionaryValue;
+}
+
+namespace gcm {
+class GCMProfileService;
+}
+
+namespace gcm_driver {
+
+// Sets the GCM infos for the gcm-internals WebUI in |results|.
+void SetGCMInternalsInfo(const gcm::GCMClient::GCMStatistics* stats,
+ gcm::GCMProfileService* profile_service,
+ PrefService* prefs,
+ base::DictionaryValue* results);
+
+} // namespace gcm_driver
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_INTERNALS_HELPER_H_
diff --git a/chromium/components/gcm_driver/gcm_profile_service.cc b/chromium/components/gcm_driver/gcm_profile_service.cc
new file mode 100644
index 00000000000..6b6bed3d2ff
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_profile_service.cc
@@ -0,0 +1,205 @@
+// Copyright (c) 2013 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.
+
+#include "components/gcm_driver/gcm_profile_service.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/memory/raw_ptr.h"
+#include "build/build_config.h"
+#include "components/gcm_driver/gcm_driver.h"
+#include "components/gcm_driver/gcm_driver_constants.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/prefs/pref_service.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+
+#if BUILDFLAG(USE_GCM_FROM_PLATFORM)
+#include "base/task/sequenced_task_runner.h"
+#include "components/gcm_driver/gcm_driver_android.h"
+#else
+#include "base/bind.h"
+#include "base/files/file_path.h"
+#include "base/memory/weak_ptr.h"
+#include "components/gcm_driver/account_tracker.h"
+#include "components/gcm_driver/gcm_account_tracker.h"
+#include "components/gcm_driver/gcm_client_factory.h"
+#include "components/gcm_driver/gcm_desktop_utils.h"
+#include "components/gcm_driver/gcm_driver_desktop.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#endif
+
+namespace gcm {
+
+#if !BUILDFLAG(USE_GCM_FROM_PLATFORM)
+// Identity observer only has actual work to do when the user is actually signed
+// in. It ensures that account tracker is taking
+class GCMProfileService::IdentityObserver
+ : public signin::IdentityManager::Observer {
+ public:
+ IdentityObserver(
+ signin::IdentityManager* identity_manager,
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
+ GCMDriver* driver);
+
+ IdentityObserver(const IdentityObserver&) = delete;
+ IdentityObserver& operator=(const IdentityObserver&) = delete;
+
+ ~IdentityObserver() override;
+
+ // signin::IdentityManager::Observer:
+ void OnPrimaryAccountChanged(
+ const signin::PrimaryAccountChangeEvent& event) override;
+
+ private:
+ void OnSyncPrimaryAccountSet(const CoreAccountInfo& primary_account_info);
+ void StartAccountTracker(
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory);
+
+ raw_ptr<GCMDriver> driver_;
+ raw_ptr<signin::IdentityManager> identity_manager_;
+ std::unique_ptr<GCMAccountTracker> gcm_account_tracker_;
+
+ // The account ID that this service is responsible for. Empty when the service
+ // is not running.
+ CoreAccountId account_id_;
+
+ base::WeakPtrFactory<GCMProfileService::IdentityObserver> weak_ptr_factory_{
+ this};
+};
+
+GCMProfileService::IdentityObserver::IdentityObserver(
+ signin::IdentityManager* identity_manager,
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
+ GCMDriver* driver)
+ : driver_(driver), identity_manager_(identity_manager) {
+ identity_manager_->AddObserver(this);
+
+ OnSyncPrimaryAccountSet(
+ identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSync));
+ StartAccountTracker(std::move(url_loader_factory));
+}
+
+GCMProfileService::IdentityObserver::~IdentityObserver() {
+ if (gcm_account_tracker_)
+ gcm_account_tracker_->Shutdown();
+ identity_manager_->RemoveObserver(this);
+}
+
+void GCMProfileService::IdentityObserver::OnPrimaryAccountChanged(
+ const signin::PrimaryAccountChangeEvent& event) {
+ switch (event.GetEventTypeFor(signin::ConsentLevel::kSync)) {
+ case signin::PrimaryAccountChangeEvent::Type::kSet:
+ OnSyncPrimaryAccountSet(event.GetCurrentState().primary_account);
+ break;
+ case signin::PrimaryAccountChangeEvent::Type::kCleared:
+ account_id_ = CoreAccountId();
+
+ // Still need to notify GCMDriver for UMA purpose.
+ driver_->OnSignedOut();
+ break;
+ case signin::PrimaryAccountChangeEvent::Type::kNone:
+ break;
+ }
+}
+
+void GCMProfileService::IdentityObserver::OnSyncPrimaryAccountSet(
+ const CoreAccountInfo& primary_account_info) {
+ // This might be called multiple times when the password changes.
+ if (primary_account_info.account_id == account_id_)
+ return;
+ account_id_ = primary_account_info.account_id;
+
+ // Still need to notify GCMDriver for UMA purpose.
+ driver_->OnSignedIn();
+}
+
+void GCMProfileService::IdentityObserver::StartAccountTracker(
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) {
+ if (gcm_account_tracker_)
+ return;
+
+ std::unique_ptr<AccountTracker> gaia_account_tracker(
+ new AccountTracker(identity_manager_));
+
+ gcm_account_tracker_ = std::make_unique<GCMAccountTracker>(
+ std::move(gaia_account_tracker), identity_manager_, driver_);
+
+ gcm_account_tracker_->Start();
+}
+
+#endif // !BUILDFLAG(USE_GCM_FROM_PLATFORM)
+
+#if BUILDFLAG(USE_GCM_FROM_PLATFORM)
+GCMProfileService::GCMProfileService(
+ base::FilePath path,
+ scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner) {
+ driver_ = std::make_unique<GCMDriverAndroid>(
+ path.Append(gcm_driver::kGCMStoreDirname), blocking_task_runner);
+}
+#else
+GCMProfileService::GCMProfileService(
+ PrefService* prefs,
+ base::FilePath path,
+ base::RepeatingCallback<void(
+ base::WeakPtr<GCMProfileService>,
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)>
+ get_socket_factory_callback,
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
+ network::NetworkConnectionTracker* network_connection_tracker,
+ version_info::Channel channel,
+ const std::string& product_category_for_subtypes,
+ signin::IdentityManager* identity_manager,
+ std::unique_ptr<GCMClientFactory> gcm_client_factory,
+ const scoped_refptr<base::SequencedTaskRunner>& ui_task_runner,
+ const scoped_refptr<base::SequencedTaskRunner>& io_task_runner,
+ scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner)
+ : identity_manager_(identity_manager),
+ url_loader_factory_(std::move(url_loader_factory)) {
+ signin::IdentityManager::AccountIdMigrationState id_migration =
+ identity_manager_->GetAccountIdMigrationState();
+ bool remove_account_mappings_with_email_key =
+ (id_migration == signin::IdentityManager::MIGRATION_IN_PROGRESS) ||
+ (id_migration == signin::IdentityManager::MIGRATION_DONE);
+ driver_ = CreateGCMDriverDesktop(
+ std::move(gcm_client_factory), prefs,
+ path.Append(gcm_driver::kGCMStoreDirname),
+ remove_account_mappings_with_email_key,
+ base::BindRepeating(get_socket_factory_callback,
+ weak_ptr_factory_.GetWeakPtr()),
+ url_loader_factory_, network_connection_tracker, channel,
+ product_category_for_subtypes, ui_task_runner, io_task_runner,
+ blocking_task_runner);
+
+ identity_observer_ = std::make_unique<IdentityObserver>(
+ identity_manager_, url_loader_factory_, driver_.get());
+}
+#endif // BUILDFLAG(USE_GCM_FROM_PLATFORM)
+
+GCMProfileService::GCMProfileService() {}
+
+GCMProfileService::~GCMProfileService() {}
+
+void GCMProfileService::Shutdown() {
+#if !BUILDFLAG(USE_GCM_FROM_PLATFORM)
+ identity_observer_.reset();
+#endif // !BUILDFLAG(USE_GCM_FROM_PLATFORM)
+ if (driver_) {
+ driver_->Shutdown();
+ driver_.reset();
+ }
+}
+
+void GCMProfileService::SetDriverForTesting(std::unique_ptr<GCMDriver> driver) {
+ driver_ = std::move(driver);
+
+#if !BUILDFLAG(USE_GCM_FROM_PLATFORM)
+ if (identity_observer_) {
+ identity_observer_ = std::make_unique<IdentityObserver>(
+ identity_manager_, url_loader_factory_, driver.get());
+ }
+#endif // !BUILDFLAG(USE_GCM_FROM_PLATFORM)
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_profile_service.h b/chromium/components/gcm_driver/gcm_profile_service.h
new file mode 100644
index 00000000000..2f06a17f832
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_profile_service.h
@@ -0,0 +1,111 @@
+// Copyright (c) 2013 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 COMPONENTS_GCM_DRIVER_GCM_PROFILE_SERVICE_H_
+#define COMPONENTS_GCM_DRIVER_GCM_PROFILE_SERVICE_H_
+
+#include <memory>
+#include <string>
+
+#include "base/callback_forward.h"
+#include "base/compiler_specific.h"
+#include "base/files/file_path.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "build/build_config.h"
+#include "components/gcm_driver/gcm_buildflags.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "components/version_info/version_info.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "services/network/public/mojom/proxy_resolving_socket.mojom.h"
+
+class PrefService;
+
+namespace base {
+class SequencedTaskRunner;
+}
+
+namespace signin {
+class IdentityManager;
+}
+
+namespace network {
+class NetworkConnectionTracker;
+class SharedURLLoaderFactory;
+}
+
+namespace gcm {
+
+class GCMClientFactory;
+class GCMDriver;
+
+// Providing GCM service, via GCMDriver.
+class GCMProfileService : public KeyedService {
+ public:
+ using GetProxyResolvingFactoryCallback = base::RepeatingCallback<void(
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)>;
+
+#if BUILDFLAG(USE_GCM_FROM_PLATFORM)
+ GCMProfileService(
+ base::FilePath path,
+ scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner);
+#else
+ GCMProfileService(
+ PrefService* prefs,
+ base::FilePath path,
+ base::RepeatingCallback<void(
+ base::WeakPtr<GCMProfileService>,
+ mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory>)>
+ get_socket_factory_callback,
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
+ network::NetworkConnectionTracker* network_connection_tracker,
+ version_info::Channel channel,
+ const std::string& product_category_for_subtypes,
+ signin::IdentityManager* identity_manager,
+ std::unique_ptr<GCMClientFactory> gcm_client_factory,
+ const scoped_refptr<base::SequencedTaskRunner>& ui_task_runner,
+ const scoped_refptr<base::SequencedTaskRunner>& io_task_runner,
+ scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner);
+#endif
+
+ GCMProfileService(const GCMProfileService&) = delete;
+ GCMProfileService& operator=(const GCMProfileService&) = delete;
+
+ ~GCMProfileService() override;
+
+ // KeyedService:
+ void Shutdown() override;
+
+ // For testing purposes.
+ void SetDriverForTesting(std::unique_ptr<GCMDriver> driver);
+
+ GCMDriver* driver() const { return driver_.get(); }
+
+ protected:
+ // Used for constructing fake GCMProfileService for testing purpose.
+ GCMProfileService();
+
+ private:
+ std::unique_ptr<GCMDriver> driver_;
+
+#if !BUILDFLAG(USE_GCM_FROM_PLATFORM)
+ raw_ptr<signin::IdentityManager> identity_manager_;
+
+ scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
+
+ // Used for both account tracker and GCM.UserSignedIn UMA.
+ class IdentityObserver;
+ std::unique_ptr<IdentityObserver> identity_observer_;
+#endif
+
+ GetProxyResolvingFactoryCallback get_socket_factory_callback_;
+
+ // WeakPtr generated by the factory must be dereferenced on the UI thread.
+ base::WeakPtrFactory<GCMProfileService> weak_ptr_factory_{this};
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_PROFILE_SERVICE_H_
diff --git a/chromium/components/gcm_driver/gcm_stats_recorder_android.cc b/chromium/components/gcm_driver/gcm_stats_recorder_android.cc
new file mode 100644
index 00000000000..dc3137be535
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_stats_recorder_android.cc
@@ -0,0 +1,148 @@
+// Copyright 2015 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.
+
+#include <stddef.h>
+
+#include "components/gcm_driver/crypto/gcm_decryption_result.h"
+#include "components/gcm_driver/gcm_stats_recorder_android.h"
+
+namespace gcm {
+
+namespace {
+
+const size_t MAX_LOGGED_ACTIVITY_COUNT = 100;
+
+const char kSuccess[] = "SUCCESS";
+const char kUnknownError[] = "UNKNOWN_ERROR";
+
+} // namespace
+
+GCMStatsRecorderAndroid::GCMStatsRecorderAndroid(Delegate* delegate)
+ : delegate_(delegate) {}
+
+GCMStatsRecorderAndroid::~GCMStatsRecorderAndroid() {}
+
+void GCMStatsRecorderAndroid::Clear() {
+ registration_activities_.clear();
+ receiving_activities_.clear();
+ decryption_failure_activities_.clear();
+}
+
+void GCMStatsRecorderAndroid::CollectActivities(
+ RecordedActivities* recorded_activities) const {
+ DCHECK(recorded_activities);
+
+ recorded_activities->registration_activities.insert(
+ recorded_activities->registration_activities.begin(),
+ registration_activities_.begin(),
+ registration_activities_.end());
+ recorded_activities->receiving_activities.insert(
+ recorded_activities->receiving_activities.begin(),
+ receiving_activities_.begin(),
+ receiving_activities_.end());
+ recorded_activities->decryption_failure_activities.insert(
+ recorded_activities->decryption_failure_activities.begin(),
+ decryption_failure_activities_.begin(),
+ decryption_failure_activities_.end());
+}
+
+void GCMStatsRecorderAndroid::RecordRegistrationSent(
+ const std::string& app_id) {
+ if (!is_recording_)
+ return;
+
+ RecordRegistration(app_id, "Registration request sent", "" /* details */);
+}
+
+void GCMStatsRecorderAndroid::RecordRegistrationResponse(
+ const std::string& app_id,
+ bool success) {
+ if (!is_recording_)
+ return;
+
+ RecordRegistration(
+ app_id, "Registration response received", success ? kSuccess
+ : kUnknownError);
+}
+
+void GCMStatsRecorderAndroid::RecordUnregistrationSent(
+ const std::string& app_id) {
+ if (!is_recording_)
+ return;
+
+ RecordRegistration(app_id, "Unregistration request sent", "" /* details */);
+}
+
+void GCMStatsRecorderAndroid::RecordUnregistrationResponse(
+ const std::string& app_id,
+ bool success) {
+ if (!is_recording_)
+ return;
+
+ RecordRegistration(
+ app_id, "Unregistration response received", success ? kSuccess
+ : kUnknownError);
+}
+
+void GCMStatsRecorderAndroid::RecordRegistration(const std::string& app_id,
+ const std::string& event,
+ const std::string& details) {
+ RegistrationActivity activity;
+ activity.app_id = app_id;
+ activity.event = event;
+ activity.details = details;
+
+ // TODO(peter): Include the |source| (sender id(s)) of the registrations.
+
+ registration_activities_.push_front(activity);
+ if (registration_activities_.size() > MAX_LOGGED_ACTIVITY_COUNT)
+ registration_activities_.pop_back();
+
+ if (delegate_)
+ delegate_->OnActivityRecorded();
+}
+
+void GCMStatsRecorderAndroid::RecordDataMessageReceived(
+ const std::string& app_id,
+ const std::string& from,
+ int message_byte_size) {
+ if (!is_recording_)
+ return;
+
+ ReceivingActivity activity;
+ activity.app_id = app_id;
+ activity.from = from;
+ activity.message_byte_size = message_byte_size;
+ activity.event = "Data msg received";
+
+ receiving_activities_.push_front(activity);
+ if (receiving_activities_.size() > MAX_LOGGED_ACTIVITY_COUNT)
+ receiving_activities_.pop_back();
+
+ if (delegate_)
+ delegate_->OnActivityRecorded();
+}
+
+void GCMStatsRecorderAndroid::RecordDecryptionFailure(
+ const std::string& app_id,
+ GCMDecryptionResult result) {
+ DCHECK_NE(result, GCMDecryptionResult::UNENCRYPTED);
+ DCHECK_NE(result, GCMDecryptionResult::DECRYPTED_DRAFT_03);
+ DCHECK_NE(result, GCMDecryptionResult::DECRYPTED_DRAFT_08);
+ if (!is_recording_)
+ return;
+
+ DecryptionFailureActivity activity;
+ activity.app_id = app_id;
+ activity.details = ToGCMDecryptionResultDetailsString(result);
+
+ decryption_failure_activities_.push_front(activity);
+ if (decryption_failure_activities_.size() > MAX_LOGGED_ACTIVITY_COUNT)
+ decryption_failure_activities_.pop_back();
+
+ if (delegate_)
+ delegate_->OnActivityRecorded();
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_stats_recorder_android.h b/chromium/components/gcm_driver/gcm_stats_recorder_android.h
new file mode 100644
index 00000000000..3bbe4c11302
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_stats_recorder_android.h
@@ -0,0 +1,99 @@
+// Copyright 2015 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 COMPONENTS_GCM_DRIVER_GCM_STATS_RECORDER_ANDROID_H_
+#define COMPONENTS_GCM_DRIVER_GCM_STATS_RECORDER_ANDROID_H_
+
+#include <string>
+
+#include "base/containers/circular_deque.h"
+#include "base/memory/raw_ptr.h"
+#include "components/gcm_driver/gcm_activity.h"
+
+namespace gcm {
+
+enum class GCMDecryptionResult;
+
+// Stats recorder for Android, used for recording stats and activities on the
+// GCM Driver level for debugging purposes. Based on the GCMStatsRecorder, as
+// defined in the GCM Engine, which does not exist on Android.
+//
+// Note that this class, different from the GCMStatsRecorder(Impl), is expected
+// to be used on the UI thread.
+class GCMStatsRecorderAndroid {
+ public:
+ // A delegate interface that allows the GCMStatsRecorderAndroid instance to
+ // interact with its container.
+ class Delegate {
+ public:
+ // Called when the GCMStatsRecorderAndroid is recording activities and a new
+ // activity has just been recorded.
+ virtual void OnActivityRecorded() = 0;
+ };
+
+ // A weak reference to |delegate| is stored, so it must outlive the recorder.
+ explicit GCMStatsRecorderAndroid(Delegate* delegate);
+
+ GCMStatsRecorderAndroid(const GCMStatsRecorderAndroid&) = delete;
+ GCMStatsRecorderAndroid& operator=(const GCMStatsRecorderAndroid&) = delete;
+
+ ~GCMStatsRecorderAndroid();
+
+ // Clears the recorded activities.
+ void Clear();
+
+ // Collects all recorded activities into |*recorded_activities|.
+ void CollectActivities(RecordedActivities* recorded_activities) const;
+
+ // Records that a registration for |app_id| has been sent.
+ void RecordRegistrationSent(const std::string& app_id);
+
+ // Records that the registration sent for |app_id| has received a response.
+ // |success| indicates whether the registration was successful.
+ void RecordRegistrationResponse(const std::string& app_id, bool success);
+
+ // Records that an unregistration for |app_id| has been sent.
+ void RecordUnregistrationSent(const std::string& app_id);
+
+ // Records that the unregistration sent for |app_id| has received a response.
+ // |success| indicates whether the unregistration was successful.
+ void RecordUnregistrationResponse(const std::string& app_id, bool success);
+
+ // Records that a data message has been received for |app_id|.
+ void RecordDataMessageReceived(const std::string& app_id,
+ const std::string& from,
+ int message_byte_size);
+
+ // Records a message decryption failure caused by |result| for |app_id|.
+ void RecordDecryptionFailure(const std::string& app_id,
+ GCMDecryptionResult result);
+
+ bool is_recording() const { return is_recording_; }
+ void set_is_recording(bool recording) { is_recording_ = recording; }
+
+ private:
+ void RecordRegistration(const std::string& app_id,
+ const std::string& event,
+ const std::string& details);
+
+ // Delegate made available by the container. May be a nullptr.
+ raw_ptr<Delegate> delegate_;
+
+ // Toggle determining whether the recorder is recording.
+ bool is_recording_ = false;
+
+ // Recorded registration activities (which includes unregistrations).
+ base::circular_deque<RegistrationActivity> registration_activities_;
+
+ // Recorded received message activities.
+ base::circular_deque<ReceivingActivity> receiving_activities_;
+
+ // Recorded message decryption failure activities.
+ base::circular_deque<DecryptionFailureActivity>
+ decryption_failure_activities_;
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_STATS_RECORDER_ANDROID_H_
diff --git a/chromium/components/gcm_driver/gcm_stats_recorder_android_unittest.cc b/chromium/components/gcm_driver/gcm_stats_recorder_android_unittest.cc
new file mode 100644
index 00000000000..d789e76907a
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_stats_recorder_android_unittest.cc
@@ -0,0 +1,116 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/gcm_stats_recorder_android.h"
+
+#include <stddef.h>
+
+#include "components/gcm_driver/crypto/gcm_decryption_result.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+
+const char kTestAppId[] = "test_app_id";
+const char kTestSenderId[] = "test_sender_id";
+
+class GCMStatsRecorderAndroidTest : public ::testing::Test,
+ public GCMStatsRecorderAndroid::Delegate {
+ public:
+ GCMStatsRecorderAndroidTest()
+ : activity_recorded_calls_(0) {}
+ ~GCMStatsRecorderAndroidTest() override {}
+
+ // GCMStatsRecorderAndroid::Delegate implementation.
+ void OnActivityRecorded() override {
+ ++activity_recorded_calls_;
+ }
+
+ size_t activity_recorded_calls() const { return activity_recorded_calls_; }
+
+ private:
+ size_t activity_recorded_calls_;
+};
+
+TEST_F(GCMStatsRecorderAndroidTest, RecordsAndCallsDelegate) {
+ GCMStatsRecorderAndroid recorder(this /* delegate */);
+ recorder.set_is_recording(true);
+
+ ASSERT_TRUE(recorder.is_recording());
+
+ EXPECT_EQ(0u, activity_recorded_calls());
+
+ recorder.RecordRegistrationSent(kTestAppId);
+ EXPECT_EQ(1u, activity_recorded_calls());
+
+ recorder.RecordRegistrationResponse(kTestAppId, true /* success */);
+ EXPECT_EQ(2u, activity_recorded_calls());
+
+ recorder.RecordUnregistrationSent(kTestAppId);
+ EXPECT_EQ(3u, activity_recorded_calls());
+
+ recorder.RecordUnregistrationResponse(kTestAppId, true /* success */);
+ EXPECT_EQ(4u, activity_recorded_calls());
+
+ recorder.RecordDataMessageReceived(kTestAppId, kTestSenderId,
+ 42 /* message_byte_size */);
+ EXPECT_EQ(5u, activity_recorded_calls());
+
+ recorder.RecordDecryptionFailure(kTestAppId,
+ GCMDecryptionResult::INVALID_PAYLOAD);
+ EXPECT_EQ(6u, activity_recorded_calls());
+
+ RecordedActivities activities;
+ recorder.CollectActivities(&activities);
+
+ EXPECT_EQ(4u, activities.registration_activities.size());
+ EXPECT_EQ(1u, activities.receiving_activities.size());
+ EXPECT_EQ(1u, activities.decryption_failure_activities.size());
+
+ recorder.CollectActivities(&activities);
+
+ EXPECT_EQ(8u, activities.registration_activities.size());
+ EXPECT_EQ(2u, activities.receiving_activities.size());
+ EXPECT_EQ(2u, activities.decryption_failure_activities.size());
+
+ recorder.Clear();
+
+ RecordedActivities empty_activities;
+ recorder.CollectActivities(&empty_activities);
+
+ EXPECT_EQ(0u, empty_activities.registration_activities.size());
+ EXPECT_EQ(0u, empty_activities.receiving_activities.size());
+ EXPECT_EQ(0u, empty_activities.decryption_failure_activities.size());
+}
+
+TEST_F(GCMStatsRecorderAndroidTest, NullDelegate) {
+ GCMStatsRecorderAndroid recorder(nullptr /* delegate */);
+ recorder.set_is_recording(true);
+
+ ASSERT_TRUE(recorder.is_recording());
+
+ recorder.RecordRegistrationSent(kTestAppId);
+
+ RecordedActivities activities;
+ recorder.CollectActivities(&activities);
+
+ EXPECT_EQ(1u, activities.registration_activities.size());
+}
+
+TEST_F(GCMStatsRecorderAndroidTest, NotRecording) {
+ GCMStatsRecorderAndroid recorder(this /* delegate */);
+ ASSERT_FALSE(recorder.is_recording());
+
+ recorder.RecordRegistrationSent(kTestAppId);
+
+ RecordedActivities activities;
+ recorder.CollectActivities(&activities);
+
+ EXPECT_EQ(0u, activities.registration_activities.size());
+}
+
+} // namespace
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_stats_recorder_impl.cc b/chromium/components/gcm_driver/gcm_stats_recorder_impl.cc
new file mode 100644
index 00000000000..d7af543792e
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_stats_recorder_impl.cc
@@ -0,0 +1,514 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/gcm_stats_recorder_impl.h"
+
+
+#include "base/containers/circular_deque.h"
+#include "base/format_macros.h"
+#include "base/logging.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+#include "components/gcm_driver/crypto/gcm_decryption_result.h"
+#include "components/gcm_driver/crypto/gcm_encryption_provider.h"
+
+namespace gcm {
+
+const uint32_t MAX_LOGGED_ACTIVITY_COUNT = 100;
+
+namespace {
+
+// Insert an item to the front of deque while maintaining the size of the deque.
+// Overflow item is discarded.
+//
+// DANGER: the returned pointer will not be valind if the queue is modified.
+template <typename T>
+T* InsertCircularBuffer(base::circular_deque<T>* q, const T& item) {
+ DCHECK(q);
+ if (q->size() > MAX_LOGGED_ACTIVITY_COUNT - 1) {
+ q->pop_back();
+ }
+ q->push_front(item);
+ return &q->front();
+}
+
+// Helper for getting string representation of the MessageSendStatus enum.
+std::string GetMessageSendStatusString(
+ gcm::MCSClient::MessageSendStatus status) {
+ switch (status) {
+ case gcm::MCSClient::QUEUED:
+ return "QUEUED";
+ case gcm::MCSClient::SENT:
+ return "SENT";
+ case gcm::MCSClient::QUEUE_SIZE_LIMIT_REACHED:
+ return "QUEUE_SIZE_LIMIT_REACHED";
+ case gcm::MCSClient::APP_QUEUE_SIZE_LIMIT_REACHED:
+ return "APP_QUEUE_SIZE_LIMIT_REACHED";
+ case gcm::MCSClient::MESSAGE_TOO_LARGE:
+ return "MESSAGE_TOO_LARGE";
+ case gcm::MCSClient::NO_CONNECTION_ON_ZERO_TTL:
+ return "NO_CONNECTION_ON_ZERO_TTL";
+ case gcm::MCSClient::TTL_EXCEEDED:
+ return "TTL_EXCEEDED";
+ case gcm::MCSClient::SEND_STATUS_COUNT:
+ NOTREACHED();
+ break;
+ }
+ return "UNKNOWN";
+}
+
+// Helper for getting string representation of the
+// ConnectionFactory::ConnectionResetReason enum.
+std::string GetConnectionResetReasonString(
+ gcm::ConnectionFactory::ConnectionResetReason reason) {
+ switch (reason) {
+ case gcm::ConnectionFactory::LOGIN_FAILURE:
+ return "LOGIN_FAILURE";
+ case gcm::ConnectionFactory::CLOSE_COMMAND:
+ return "CLOSE_COMMAND";
+ case gcm::ConnectionFactory::HEARTBEAT_FAILURE:
+ return "HEARTBEAT_FAILURE";
+ case gcm::ConnectionFactory::SOCKET_FAILURE:
+ return "SOCKET_FAILURE";
+ case gcm::ConnectionFactory::NETWORK_CHANGE:
+ return "NETWORK_CHANGE";
+ case gcm::ConnectionFactory::NEW_HEARTBEAT_INTERVAL:
+ return "NEW_HEARTBEAT_INTERVAL";
+ case gcm::ConnectionFactory::CONNECTION_RESET_COUNT:
+ NOTREACHED();
+ break;
+ }
+ return "UNKNOWN_REASON";
+}
+
+// Helper for getting string representation of the RegistrationRequest::Status
+// enum.
+std::string GetRegistrationStatusString(
+ gcm::RegistrationRequest::Status status) {
+ switch (status) {
+ case gcm::RegistrationRequest::SUCCESS:
+ return "SUCCESS";
+ case gcm::RegistrationRequest::INVALID_PARAMETERS:
+ return "INVALID_PARAMETERS";
+ case gcm::RegistrationRequest::INVALID_SENDER:
+ return "INVALID_SENDER";
+ case gcm::RegistrationRequest::AUTHENTICATION_FAILED:
+ return "AUTHENTICATION_FAILED";
+ case gcm::RegistrationRequest::DEVICE_REGISTRATION_ERROR:
+ return "DEVICE_REGISTRATION_ERROR";
+ case gcm::RegistrationRequest::UNKNOWN_ERROR:
+ return "UNKNOWN_ERROR";
+ case gcm::RegistrationRequest::URL_FETCHING_FAILED:
+ return "URL_FETCHING_FAILED";
+ case gcm::RegistrationRequest::HTTP_NOT_OK:
+ return "HTTP_NOT_OK";
+ case gcm::RegistrationRequest::NO_RESPONSE_BODY:
+ return "NO_RESPONSE_BODY";
+ case gcm::RegistrationRequest::REACHED_MAX_RETRIES:
+ return "REACHED_MAX_RETRIES";
+ case gcm::RegistrationRequest::RESPONSE_PARSING_FAILED:
+ return "RESPONSE_PARSING_FAILED";
+ case gcm::RegistrationRequest::INTERNAL_SERVER_ERROR:
+ return "INTERNAL_SERVER_ERROR";
+ case gcm::RegistrationRequest::QUOTA_EXCEEDED:
+ return "QUOTA_EXCEEDED";
+ case gcm::RegistrationRequest::TOO_MANY_REGISTRATIONS:
+ return "TOO_MANY_REGISTRATIONS";
+ case gcm::RegistrationRequest::STATUS_COUNT:
+ NOTREACHED();
+ break;
+ }
+ return "UNKNOWN_STATUS";
+}
+
+// Helper for getting string representation of the RegistrationRequest::Status
+// enum.
+std::string GetUnregistrationStatusString(
+ gcm::UnregistrationRequest::Status status) {
+ switch (status) {
+ case gcm::UnregistrationRequest::SUCCESS:
+ return "SUCCESS";
+ case gcm::UnregistrationRequest::URL_FETCHING_FAILED:
+ return "URL_FETCHING_FAILED";
+ case gcm::UnregistrationRequest::NO_RESPONSE_BODY:
+ return "NO_RESPONSE_BODY";
+ case gcm::UnregistrationRequest::RESPONSE_PARSING_FAILED:
+ return "RESPONSE_PARSING_FAILED";
+ case gcm::UnregistrationRequest::INCORRECT_APP_ID:
+ return "INCORRECT_APP_ID";
+ case gcm::UnregistrationRequest::INVALID_PARAMETERS:
+ return "INVALID_PARAMETERS";
+ case gcm::UnregistrationRequest::SERVICE_UNAVAILABLE:
+ return "SERVICE_UNAVAILABLE";
+ case gcm::UnregistrationRequest::INTERNAL_SERVER_ERROR:
+ return "INTERNAL_SERVER_ERROR";
+ case gcm::UnregistrationRequest::HTTP_NOT_OK:
+ return "HTTP_NOT_OK";
+ case gcm::UnregistrationRequest::UNKNOWN_ERROR:
+ return "UNKNOWN_ERROR";
+ case gcm::UnregistrationRequest::REACHED_MAX_RETRIES:
+ return "REACHED_MAX_RETRIES";
+ case gcm::UnregistrationRequest::DEVICE_REGISTRATION_ERROR:
+ return "DEVICE_REGISTRATION_ERROR";
+ case gcm::UnregistrationRequest::UNREGISTRATION_STATUS_COUNT:
+ NOTREACHED();
+ break;
+ }
+ return "UNKNOWN_STATUS";
+}
+
+} // namespace
+
+GCMStatsRecorderImpl::GCMStatsRecorderImpl()
+ : is_recording_(false), delegate_(nullptr) {}
+
+GCMStatsRecorderImpl::~GCMStatsRecorderImpl() = default;
+
+void GCMStatsRecorderImpl::SetDelegate(Delegate* delegate) {
+ delegate_ = delegate;
+}
+
+void GCMStatsRecorderImpl::Clear() {
+ checkin_activities_.clear();
+ connection_activities_.clear();
+ registration_activities_.clear();
+ receiving_activities_.clear();
+ sending_activities_.clear();
+ decryption_failure_activities_.clear();
+}
+
+void GCMStatsRecorderImpl::NotifyActivityRecorded() {
+ if (delegate_)
+ delegate_->OnActivityRecorded();
+}
+
+void GCMStatsRecorderImpl::RecordDecryptionFailure(const std::string& app_id,
+ GCMDecryptionResult result) {
+ DCHECK_NE(result, GCMDecryptionResult::UNENCRYPTED);
+ DCHECK_NE(result, GCMDecryptionResult::DECRYPTED_DRAFT_03);
+ DCHECK_NE(result, GCMDecryptionResult::DECRYPTED_DRAFT_08);
+ if (!is_recording_)
+ return;
+
+ DecryptionFailureActivity data;
+ DecryptionFailureActivity* inserted_data = InsertCircularBuffer(
+ &decryption_failure_activities_, data);
+ inserted_data->app_id = app_id;
+ inserted_data->details = ToGCMDecryptionResultDetailsString(result);
+
+ NotifyActivityRecorded();
+}
+
+void GCMStatsRecorderImpl::RecordCheckin(
+ const std::string& event,
+ const std::string& details) {
+ CheckinActivity data;
+ CheckinActivity* inserted_data = InsertCircularBuffer(
+ &checkin_activities_, data);
+ inserted_data->event = event;
+ inserted_data->details = details;
+ NotifyActivityRecorded();
+}
+
+void GCMStatsRecorderImpl::RecordCheckinInitiated(uint64_t android_id) {
+ if (!is_recording_)
+ return;
+ RecordCheckin("Checkin initiated",
+ base::StringPrintf("Android Id: %" PRIu64, android_id));
+}
+
+void GCMStatsRecorderImpl::RecordCheckinDelayedDueToBackoff(
+ int64_t delay_msec) {
+ if (!is_recording_)
+ return;
+ RecordCheckin("Checkin backoff",
+ base::StringPrintf("Delayed for %" PRId64 " msec",
+ delay_msec));
+}
+
+void GCMStatsRecorderImpl::RecordCheckinSuccess() {
+ if (!is_recording_)
+ return;
+ RecordCheckin("Checkin succeeded", std::string());
+}
+
+void GCMStatsRecorderImpl::RecordCheckinFailure(const std::string& status,
+ bool will_retry) {
+ if (!is_recording_)
+ return;
+ RecordCheckin("Checkin failed", base::StringPrintf(
+ "%s.%s",
+ status.c_str(),
+ will_retry ? " Will retry." : "Will not retry."));
+}
+
+void GCMStatsRecorderImpl::RecordConnection(
+ const std::string& event,
+ const std::string& details) {
+ ConnectionActivity data;
+ ConnectionActivity* inserted_data = InsertCircularBuffer(
+ &connection_activities_, data);
+ inserted_data->event = event;
+ inserted_data->details = details;
+ NotifyActivityRecorded();
+}
+
+void GCMStatsRecorderImpl::RecordConnectionInitiated(const std::string& host) {
+ last_connection_initiation_time_ = base::TimeTicks::Now();
+ if (!is_recording_)
+ return;
+
+ RecordConnection("Connection initiated", host);
+}
+
+void GCMStatsRecorderImpl::RecordConnectionDelayedDueToBackoff(
+ int64_t delay_msec) {
+ if (!is_recording_)
+ return;
+
+ RecordConnection("Connection backoff",
+ base::StringPrintf("Delayed for %" PRId64 " msec",
+ delay_msec));
+}
+
+void GCMStatsRecorderImpl::RecordConnectionSuccess() {
+ DCHECK(!last_connection_initiation_time_.is_null());
+ UMA_HISTOGRAM_MEDIUM_TIMES(
+ "GCM.ConnectionLatency",
+ (base::TimeTicks::Now() - last_connection_initiation_time_));
+ last_connection_initiation_time_ = base::TimeTicks();
+ if (!is_recording_)
+ return;
+ RecordConnection("Connection succeeded", std::string());
+}
+
+void GCMStatsRecorderImpl::RecordConnectionFailure(int network_error) {
+ if (!is_recording_)
+ return;
+ RecordConnection("Connection failed",
+ base::StringPrintf("With network error %d", network_error));
+}
+
+void GCMStatsRecorderImpl::RecordConnectionResetSignaled(
+ ConnectionFactory::ConnectionResetReason reason) {
+ if (!is_recording_)
+ return;
+ RecordConnection("Connection reset",
+ GetConnectionResetReasonString(reason));
+}
+
+void GCMStatsRecorderImpl::RecordRegistration(
+ const std::string& app_id,
+ const std::string& source,
+ const std::string& event,
+ const std::string& details) {
+ RegistrationActivity data;
+ RegistrationActivity* inserted_data = InsertCircularBuffer(
+ &registration_activities_, data);
+ inserted_data->app_id = app_id;
+ inserted_data->source = source;
+ inserted_data->event = event;
+ inserted_data->details = details;
+ NotifyActivityRecorded();
+}
+
+void GCMStatsRecorderImpl::RecordRegistrationSent(
+ const std::string& app_id,
+ const std::string& sender_ids) {
+ UMA_HISTOGRAM_COUNTS_1M("GCM.RegistrationRequest", 1);
+ if (!is_recording_)
+ return;
+ RecordRegistration(app_id, sender_ids,
+ "Registration request sent", std::string());
+}
+
+void GCMStatsRecorderImpl::RecordRegistrationResponse(
+ const std::string& app_id,
+ const std::string& source,
+ RegistrationRequest::Status status) {
+ if (!is_recording_)
+ return;
+ RecordRegistration(app_id, source,
+ "Registration response received",
+ GetRegistrationStatusString(status));
+}
+
+void GCMStatsRecorderImpl::RecordRegistrationRetryDelayed(
+ const std::string& app_id,
+ const std::string& source,
+ int64_t delay_msec,
+ int retries_left) {
+ if (!is_recording_)
+ return;
+ RecordRegistration(
+ app_id,
+ source,
+ "Registration retry delayed",
+ base::StringPrintf("Delayed for %" PRId64 " msec, retries left: %d",
+ delay_msec,
+ retries_left));
+}
+
+void GCMStatsRecorderImpl::RecordUnregistrationSent(
+ const std::string& app_id, const std::string& source) {
+ UMA_HISTOGRAM_COUNTS_1M("GCM.UnregistrationRequest", 1);
+ if (!is_recording_)
+ return;
+ RecordRegistration(app_id, source, "Unregistration request sent",
+ std::string());
+}
+
+void GCMStatsRecorderImpl::RecordUnregistrationResponse(
+ const std::string& app_id,
+ const std::string& source,
+ UnregistrationRequest::Status status) {
+ if (!is_recording_)
+ return;
+ RecordRegistration(app_id,
+ source,
+ "Unregistration response received",
+ GetUnregistrationStatusString(status));
+}
+
+void GCMStatsRecorderImpl::RecordUnregistrationRetryDelayed(
+ const std::string& app_id,
+ const std::string& source,
+ int64_t delay_msec,
+ int retries_left) {
+ if (!is_recording_)
+ return;
+ RecordRegistration(
+ app_id,
+ source,
+ "Unregistration retry delayed",
+ base::StringPrintf("Delayed for %" PRId64 " msec, retries left: %d",
+ delay_msec,
+ retries_left));
+}
+
+void GCMStatsRecorderImpl::RecordReceiving(
+ const std::string& app_id,
+ const std::string& from,
+ int message_byte_size,
+ const std::string& event,
+ const std::string& details) {
+ ReceivingActivity data;
+ ReceivingActivity* inserted_data = InsertCircularBuffer(
+ &receiving_activities_, data);
+ inserted_data->app_id = app_id;
+ inserted_data->from = from;
+ inserted_data->message_byte_size = message_byte_size;
+ inserted_data->event = event;
+ inserted_data->details = details;
+ NotifyActivityRecorded();
+}
+
+void GCMStatsRecorderImpl::RecordDataMessageReceived(
+ const std::string& app_id,
+ const std::string& from,
+ int message_byte_size,
+ ReceivedMessageType message_type) {
+ if (!is_recording_)
+ return;
+
+ switch (message_type) {
+ case GCMStatsRecorderImpl::DATA_MESSAGE:
+ RecordReceiving(app_id, from, message_byte_size, "Data msg received",
+ std::string());
+ break;
+ case GCMStatsRecorderImpl::DELETED_MESSAGES:
+ RecordReceiving(app_id, from, message_byte_size, "Data msg received",
+ "Message has been deleted on server");
+ break;
+ }
+}
+
+void GCMStatsRecorderImpl::CollectActivities(
+ RecordedActivities* recorded_activities) const {
+ recorded_activities->checkin_activities.insert(
+ recorded_activities->checkin_activities.begin(),
+ checkin_activities_.begin(),
+ checkin_activities_.end());
+ recorded_activities->connection_activities.insert(
+ recorded_activities->connection_activities.begin(),
+ connection_activities_.begin(),
+ connection_activities_.end());
+ recorded_activities->registration_activities.insert(
+ recorded_activities->registration_activities.begin(),
+ registration_activities_.begin(),
+ registration_activities_.end());
+ recorded_activities->receiving_activities.insert(
+ recorded_activities->receiving_activities.begin(),
+ receiving_activities_.begin(),
+ receiving_activities_.end());
+ recorded_activities->sending_activities.insert(
+ recorded_activities->sending_activities.begin(),
+ sending_activities_.begin(),
+ sending_activities_.end());
+ recorded_activities->decryption_failure_activities.insert(
+ recorded_activities->decryption_failure_activities.begin(),
+ decryption_failure_activities_.begin(),
+ decryption_failure_activities_.end());
+}
+
+void GCMStatsRecorderImpl::RecordSending(const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id,
+ const std::string& event,
+ const std::string& details) {
+ SendingActivity data;
+ SendingActivity* inserted_data = InsertCircularBuffer(
+ &sending_activities_, data);
+ inserted_data->app_id = app_id;
+ inserted_data->receiver_id = receiver_id;
+ inserted_data->message_id = message_id;
+ inserted_data->event = event;
+ inserted_data->details = details;
+ NotifyActivityRecorded();
+}
+
+void GCMStatsRecorderImpl::RecordDataSentToWire(
+ const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id,
+ int queued) {
+ if (!is_recording_)
+ return;
+ RecordSending(app_id, receiver_id, message_id, "Data msg sent to wire",
+ base::StringPrintf("Msg queued for %d seconds", queued));
+}
+
+void GCMStatsRecorderImpl::RecordNotifySendStatus(
+ const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id,
+ gcm::MCSClient::MessageSendStatus status,
+ int byte_size,
+ int ttl) {
+ UMA_HISTOGRAM_ENUMERATION("GCM.SendMessageStatus", status,
+ gcm::MCSClient::SEND_STATUS_COUNT);
+ if (!is_recording_)
+ return;
+ RecordSending(
+ app_id,
+ receiver_id,
+ message_id,
+ base::StringPrintf("SEND status: %s",
+ GetMessageSendStatusString(status).c_str()),
+ base::StringPrintf("Msg size: %d bytes, TTL: %d", byte_size, ttl));
+}
+
+void GCMStatsRecorderImpl::RecordIncomingSendError(
+ const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id) {
+ UMA_HISTOGRAM_COUNTS_1M("GCM.IncomingSendErrors", 1);
+ if (!is_recording_)
+ return;
+ RecordSending(app_id, receiver_id, message_id, "Received 'send error' msg",
+ std::string());
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/gcm_stats_recorder_impl.h b/chromium/components/gcm_driver/gcm_stats_recorder_impl.h
new file mode 100644
index 00000000000..fca73d2ed0f
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_stats_recorder_impl.h
@@ -0,0 +1,171 @@
+// Copyright 2014 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 COMPONENTS_GCM_DRIVER_GCM_STATS_RECORDER_IMPL_H_
+#define COMPONENTS_GCM_DRIVER_GCM_STATS_RECORDER_IMPL_H_
+
+#include <stdint.h>
+
+#include <string>
+
+#include "base/containers/circular_deque.h"
+#include "base/memory/raw_ptr.h"
+#include "base/time/time.h"
+#include "components/gcm_driver/gcm_activity.h"
+#include "google_apis/gcm/engine/connection_factory.h"
+#include "google_apis/gcm/engine/mcs_client.h"
+#include "google_apis/gcm/engine/registration_request.h"
+#include "google_apis/gcm/engine/unregistration_request.h"
+#include "google_apis/gcm/monitoring/gcm_stats_recorder.h"
+
+namespace gcm {
+
+enum class GCMDecryptionResult;
+
+// Records GCM internal stats and activities for debugging purpose. Recording
+// can be turned on/off by calling set_is_recording(...) function. It is turned
+// off by default.
+// This class is not thread safe. It is meant to be owned by a gcm client
+// instance.
+class GCMStatsRecorderImpl : public GCMStatsRecorder {
+ public:
+ GCMStatsRecorderImpl();
+
+ GCMStatsRecorderImpl(const GCMStatsRecorderImpl&) = delete;
+ GCMStatsRecorderImpl& operator=(const GCMStatsRecorderImpl&) = delete;
+
+ ~GCMStatsRecorderImpl() override;
+
+ // Set a delegate to receive callback from the recorder.
+ void SetDelegate(Delegate* delegate);
+
+ // Clear all recorded activities.
+ void Clear();
+
+ // Records a message decryption failure caused by |result| for |app_id|.
+ void RecordDecryptionFailure(const std::string& app_id,
+ GCMDecryptionResult result);
+
+ // GCMStatsRecorder implementation:
+ void RecordCheckinInitiated(uint64_t android_id) override;
+ void RecordCheckinDelayedDueToBackoff(int64_t delay_msec) override;
+ void RecordCheckinSuccess() override;
+ void RecordCheckinFailure(const std::string& status,
+ bool will_retry) override;
+ void RecordConnectionInitiated(const std::string& host) override;
+ void RecordConnectionDelayedDueToBackoff(int64_t delay_msec) override;
+ void RecordConnectionSuccess() override;
+ void RecordConnectionFailure(int network_error) override;
+ void RecordConnectionResetSignaled(
+ ConnectionFactory::ConnectionResetReason reason) override;
+ void RecordRegistrationSent(const std::string& app_id,
+ const std::string& source) override;
+ void RecordRegistrationResponse(const std::string& app_id,
+ const std::string& source,
+ RegistrationRequest::Status status) override;
+ void RecordRegistrationRetryDelayed(const std::string& app_id,
+ const std::string& source,
+ int64_t delay_msec,
+ int retries_left) override;
+ void RecordUnregistrationSent(const std::string& app_id,
+ const std::string& source) override;
+ void RecordUnregistrationResponse(
+ const std::string& app_id,
+ const std::string& source,
+ UnregistrationRequest::Status status) override;
+ void RecordUnregistrationRetryDelayed(const std::string& app_id,
+ const std::string& source,
+ int64_t delay_msec,
+ int retries_left) override;
+ void RecordDataMessageReceived(const std::string& app_id,
+ const std::string& from,
+ int message_byte_size,
+ ReceivedMessageType message_type) override;
+ void RecordDataSentToWire(const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id,
+ int queued) override;
+ void RecordNotifySendStatus(const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id,
+ MCSClient::MessageSendStatus status,
+ int byte_size,
+ int ttl) override;
+ void RecordIncomingSendError(const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id) override;
+
+ // Collect all recorded activities into |*recorded_activities|.
+ void CollectActivities(RecordedActivities* recorded_activities) const;
+
+ bool is_recording() const { return is_recording_; }
+ void set_is_recording(bool recording) { is_recording_ = recording; }
+
+ const base::circular_deque<CheckinActivity>& checkin_activities() const {
+ return checkin_activities_;
+ }
+ const base::circular_deque<ConnectionActivity>& connection_activities()
+ const {
+ return connection_activities_;
+ }
+ const base::circular_deque<RegistrationActivity>& registration_activities()
+ const {
+ return registration_activities_;
+ }
+ const base::circular_deque<ReceivingActivity>& receiving_activities() const {
+ return receiving_activities_;
+ }
+ const base::circular_deque<SendingActivity>& sending_activities() const {
+ return sending_activities_;
+ }
+ const base::circular_deque<DecryptionFailureActivity>&
+ decryption_failure_activities() const {
+ return decryption_failure_activities_;
+ }
+
+ protected:
+ // Notify the recorder delegate, if it exists, that an activity has been
+ // recorded.
+ void NotifyActivityRecorded();
+
+ void RecordCheckin(const std::string& event,
+ const std::string& details);
+
+ void RecordConnection(const std::string& event,
+ const std::string& details);
+
+ void RecordRegistration(const std::string& app_id,
+ const std::string& source,
+ const std::string& event,
+ const std::string& details);
+
+ void RecordReceiving(const std::string& app_id,
+ const std::string& from,
+ int message_byte_size,
+ const std::string& event,
+ const std::string& details);
+
+ void RecordSending(const std::string& app_id,
+ const std::string& receiver_id,
+ const std::string& message_id,
+ const std::string& event,
+ const std::string& details);
+
+ bool is_recording_;
+ raw_ptr<Delegate> delegate_;
+
+ base::circular_deque<CheckinActivity> checkin_activities_;
+ base::circular_deque<ConnectionActivity> connection_activities_;
+ base::circular_deque<RegistrationActivity> registration_activities_;
+ base::circular_deque<ReceivingActivity> receiving_activities_;
+ base::circular_deque<SendingActivity> sending_activities_;
+ base::circular_deque<DecryptionFailureActivity>
+ decryption_failure_activities_;
+
+ base::TimeTicks last_connection_initiation_time_;
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_GCM_STATS_RECORDER_IMPL_H_
diff --git a/chromium/components/gcm_driver/gcm_stats_recorder_impl_unittest.cc b/chromium/components/gcm_driver/gcm_stats_recorder_impl_unittest.cc
new file mode 100644
index 00000000000..f9076b99ad3
--- /dev/null
+++ b/chromium/components/gcm_driver/gcm_stats_recorder_impl_unittest.cc
@@ -0,0 +1,536 @@
+// Copyright 2014 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.
+
+#include "components/gcm_driver/gcm_stats_recorder_impl.h"
+
+#include <stdint.h>
+
+#include <string>
+
+#include "base/containers/circular_deque.h"
+#include "components/gcm_driver/crypto/gcm_decryption_result.h"
+#include "components/gcm_driver/crypto/gcm_encryption_provider.h"
+#include "google_apis/gcm/engine/mcs_client.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gcm {
+
+namespace {
+
+static uint64_t kAndroidId = 4U;
+static const char kCheckinStatus[] = "URL_FETCHING_FAILED";
+static const char kHost[] = "www.example.com";
+static const char kAppId[] = "app id 1";
+static const char kFrom[] = "from";
+static const char kSenderIds[] = "s1,s2";
+static const char kReceiverId[] = "receiver 1";
+static const char kMessageId[] = "message id 1";
+static const int kQueuedSec = 5;
+static const gcm::MCSClient::MessageSendStatus kMessageSendStatus =
+ gcm::MCSClient::QUEUED;
+static const int kByteSize = 99;
+static const int kTTL = 7;
+static const int kRetries = 3;
+static const int64_t kDelay = 15000;
+static const ConnectionFactory::ConnectionResetReason kReason =
+ ConnectionFactory::NETWORK_CHANGE;
+static const int kNetworkError = 1;
+
+static const RegistrationRequest::Status kRegistrationStatus =
+ RegistrationRequest::SUCCESS;
+static const UnregistrationRequest::Status kUnregistrationStatus =
+ UnregistrationRequest::SUCCESS;
+
+static const char kCheckinInitiatedEvent[] = "Checkin initiated";
+static const char kCheckinInitiatedDetails[] = "Android Id: 4";
+static const char kCheckinDelayedDueToBackoffEvent[] = "Checkin backoff";
+static const char kCheckinDelayedDueToBackoffDetails[] =
+ "Delayed for 15000 msec";
+static const char kCheckinSuccessEvent[] = "Checkin succeeded";
+static const char kCheckinSuccessDetails[] = "";
+static const char kCheckinFailureEvent[] = "Checkin failed";
+static const char kCheckinFailureDetails[] = "URL_FETCHING_FAILED. Will retry.";
+
+static const char kConnectionInitiatedEvent[] = "Connection initiated";
+static const char kConnectionInitiatedDetails[] = "www.example.com";
+static const char kConnectionDelayedDueToBackoffEvent[] = "Connection backoff";
+static const char kConnectionDelayedDueToBackoffDetails[] =
+ "Delayed for 15000 msec";
+static const char kConnectionSuccessEvent[] = "Connection succeeded";
+static const char kConnectionSuccessDetails[] = "";
+static const char kConnectionFailureEvent[] = "Connection failed";
+static const char kConnectionFailureDetails[] = "With network error 1";
+static const char kConnectionResetSignaledEvent[] = "Connection reset";
+static const char kConnectionResetSignaledDetails[] = "NETWORK_CHANGE";
+
+static const char kRegistrationSentEvent[] = "Registration request sent";
+static const char kRegistrationSentDetails[] = "";
+static const char kRegistrationResponseEvent[] =
+ "Registration response received";
+static const char kRegistrationResponseDetails[] = "SUCCESS";
+static const char kRegistrationRetryDelayedEvent[] =
+ "Registration retry delayed";
+static const char kRegistrationRetryDelayedDetails[] =
+ "Delayed for 15000 msec, retries left: 3";
+static const char kUnregistrationSentEvent[] = "Unregistration request sent";
+static const char kUnregistrationSentDetails[] = "";
+static const char kUnregistrationResponseEvent[] =
+ "Unregistration response received";
+static const char kUnregistrationResponseDetails[] = "SUCCESS";
+static const char kUnregistrationRetryDelayedEvent[] =
+ "Unregistration retry delayed";
+static const char kUnregistrationRetryDelayedDetails[] =
+ "Delayed for 15000 msec, retries left: 3";
+
+static const char kDataReceivedEvent[] = "Data msg received";
+static const char kDataReceivedDetails[] = "";
+static const char kDataDeletedMessageEvent[] = "Data msg received";
+static const char kDataDeletedMessageDetails[] =
+ "Message has been deleted on server";
+
+static const char kDataSentToWireEvent[] = "Data msg sent to wire";
+static const char kSentToWireDetails[] = "Msg queued for 5 seconds";
+static const char kNotifySendStatusEvent[] = "SEND status: QUEUED";
+static const char kNotifySendStatusDetails[] = "Msg size: 99 bytes, TTL: 7";
+static const char kIncomingSendErrorEvent[] = "Received 'send error' msg";
+static const char kIncomingSendErrorDetails[] = "";
+
+static const GCMDecryptionResult kDecryptionResultFailure =
+ GCMDecryptionResult::INVALID_PAYLOAD;
+
+} // namespace
+
+class GCMStatsRecorderImplTest : public testing::Test {
+ public:
+ GCMStatsRecorderImplTest();
+ ~GCMStatsRecorderImplTest() override;
+ void SetUp() override;
+
+ void VerifyRecordedCheckinCount(int expected_count) {
+ EXPECT_EQ(expected_count,
+ static_cast<int>(recorder_.checkin_activities().size()));
+ }
+ void VerifyRecordedConnectionCount(int expected_count) {
+ EXPECT_EQ(expected_count,
+ static_cast<int>(recorder_.connection_activities().size()));
+ }
+ void VerifyRecordedRegistrationCount(int expected_count) {
+ EXPECT_EQ(expected_count,
+ static_cast<int>(recorder_.registration_activities().size()));
+ }
+ void VerifyRecordedReceivingCount(int expected_count) {
+ EXPECT_EQ(expected_count,
+ static_cast<int>(recorder_.receiving_activities().size()));
+ }
+ void VerifyRecordedSendingCount(int expected_count) {
+ EXPECT_EQ(expected_count,
+ static_cast<int>(recorder_.sending_activities().size()));
+ }
+ void VerifyRecordedDecryptionFailureCount(int expected_count) {
+ EXPECT_EQ(
+ expected_count,
+ static_cast<int>(recorder_.decryption_failure_activities().size()));
+ }
+ void VerifyAllActivityQueueEmpty(const std::string& remark) {
+ EXPECT_TRUE(recorder_.checkin_activities().empty()) << remark;
+ EXPECT_TRUE(recorder_.connection_activities().empty()) << remark;
+ EXPECT_TRUE(recorder_.registration_activities().empty()) << remark;
+ EXPECT_TRUE(recorder_.receiving_activities().empty()) << remark;
+ EXPECT_TRUE(recorder_.sending_activities().empty()) << remark;
+ EXPECT_TRUE(recorder_.decryption_failure_activities().empty()) << remark;
+ }
+
+ void VerifyCheckinInitiated(const std::string& remark) {
+ VerifyCheckin(recorder_.checkin_activities(),
+ kCheckinInitiatedEvent,
+ kCheckinInitiatedDetails,
+ remark);
+ }
+
+ void VerifyCheckinDelayedDueToBackoff(const std::string& remark) {
+ VerifyCheckin(recorder_.checkin_activities(),
+ kCheckinDelayedDueToBackoffEvent,
+ kCheckinDelayedDueToBackoffDetails,
+ remark);
+ }
+
+ void VerifyCheckinSuccess(const std::string& remark) {
+ VerifyCheckin(recorder_.checkin_activities(),
+ kCheckinSuccessEvent,
+ kCheckinSuccessDetails,
+ remark);
+ }
+
+ void VerifyCheckinFailure(const std::string& remark) {
+ VerifyCheckin(recorder_.checkin_activities(),
+ kCheckinFailureEvent,
+ kCheckinFailureDetails,
+ remark);
+ }
+
+ void VerifyConnectionInitiated(const std::string& remark) {
+ VerifyConnection(recorder_.connection_activities(),
+ kConnectionInitiatedEvent,
+ kConnectionInitiatedDetails,
+ remark);
+ }
+
+ void VerifyConnectionDelayedDueToBackoff(const std::string& remark) {
+ VerifyConnection(recorder_.connection_activities(),
+ kConnectionDelayedDueToBackoffEvent,
+ kConnectionDelayedDueToBackoffDetails,
+ remark);
+ }
+
+ void VerifyConnectionSuccess(const std::string& remark) {
+ VerifyConnection(recorder_.connection_activities(),
+ kConnectionSuccessEvent,
+ kConnectionSuccessDetails,
+ remark);
+ }
+
+ void VerifyConnectionFailure(const std::string& remark) {
+ VerifyConnection(recorder_.connection_activities(),
+ kConnectionFailureEvent,
+ kConnectionFailureDetails,
+ remark);
+ }
+
+ void VerifyConnectionResetSignaled(const std::string& remark) {
+ VerifyConnection(recorder_.connection_activities(),
+ kConnectionResetSignaledEvent,
+ kConnectionResetSignaledDetails,
+ remark);
+ }
+
+ void VerifyRegistrationSent(const std::string& remark) {
+ VerifyRegistration(recorder_.registration_activities(),
+ kSenderIds,
+ kRegistrationSentEvent,
+ kRegistrationSentDetails,
+ remark);
+ }
+
+ void VerifyRegistrationResponse(const std::string& remark) {
+ VerifyRegistration(recorder_.registration_activities(),
+ kSenderIds,
+ kRegistrationResponseEvent,
+ kRegistrationResponseDetails,
+ remark);
+ }
+
+ void VerifyRegistrationRetryRequested(const std::string& remark) {
+ VerifyRegistration(recorder_.registration_activities(),
+ kSenderIds,
+ kRegistrationRetryDelayedEvent,
+ kRegistrationRetryDelayedDetails,
+ remark);
+ }
+
+ void VerifyUnregistrationSent(const std::string& remark) {
+ VerifyRegistration(recorder_.registration_activities(),
+ kSenderIds,
+ kUnregistrationSentEvent,
+ kUnregistrationSentDetails,
+ remark);
+ }
+
+ void VerifyUnregistrationResponse(const std::string& remark) {
+ VerifyRegistration(recorder_.registration_activities(),
+ kSenderIds,
+ kUnregistrationResponseEvent,
+ kUnregistrationResponseDetails,
+ remark);
+ }
+
+ void VerifyUnregistrationRetryDelayed(const std::string& remark) {
+ VerifyRegistration(recorder_.registration_activities(),
+ kSenderIds,
+ kUnregistrationRetryDelayedEvent,
+ kUnregistrationRetryDelayedDetails,
+ remark);
+ }
+
+ void VerifyDataMessageReceived(const std::string& remark) {
+ VerifyReceivingData(recorder_.receiving_activities(),
+ kDataReceivedEvent,
+ kDataReceivedDetails,
+ remark);
+ }
+
+ void VerifyDataDeletedMessage(const std::string& remark) {
+ VerifyReceivingData(recorder_.receiving_activities(),
+ kDataDeletedMessageEvent,
+ kDataDeletedMessageDetails,
+ remark);
+ }
+
+ void VerifyDataSentToWire(const std::string& remark) {
+ VerifySendingData(recorder_.sending_activities(),
+ kDataSentToWireEvent,
+ kSentToWireDetails,
+ remark);
+ }
+
+ void VerifyNotifySendStatus(const std::string& remark) {
+ VerifySendingData(recorder_.sending_activities(),
+ kNotifySendStatusEvent,
+ kNotifySendStatusDetails,
+ remark);
+ }
+
+ void VerifyIncomingSendError(const std::string& remark) {
+ VerifySendingData(recorder_.sending_activities(),
+ kIncomingSendErrorEvent,
+ kIncomingSendErrorDetails,
+ remark);
+ }
+
+ void VerifyRecordedDecryptionFailure(const std::string& remark) {
+ const auto& queue = recorder_.decryption_failure_activities();
+
+ EXPECT_EQ(kAppId, queue.front().app_id) << remark;
+ EXPECT_EQ(ToGCMDecryptionResultDetailsString(kDecryptionResultFailure),
+ queue.front().details)
+ << remark;
+ }
+
+ protected:
+ void VerifyCheckin(const base::circular_deque<CheckinActivity>& queue,
+ const std::string& event,
+ const std::string& details,
+ const std::string& remark) {
+ EXPECT_EQ(event, queue.front().event) << remark;
+ EXPECT_EQ(details, queue.front().details) << remark;
+ }
+
+ void VerifyConnection(const base::circular_deque<ConnectionActivity>& queue,
+ const std::string& event,
+ const std::string& details,
+ const std::string& remark) {
+ EXPECT_EQ(event, queue.front().event) << remark;
+ EXPECT_EQ(details, queue.front().details) << remark;
+ }
+
+ void VerifyRegistration(
+ const base::circular_deque<RegistrationActivity>& queue,
+ const std::string& source,
+ const std::string& event,
+ const std::string& details,
+ const std::string& remark) {
+ EXPECT_EQ(kAppId, queue.front().app_id) << remark;
+ EXPECT_EQ(source, queue.front().source) << remark;
+ EXPECT_EQ(event, queue.front().event) << remark;
+ EXPECT_EQ(details, queue.front().details) << remark;
+ }
+
+ void VerifyReceivingData(const base::circular_deque<ReceivingActivity>& queue,
+ const std::string& event,
+ const std::string& details,
+ const std::string& remark) {
+ EXPECT_EQ(kAppId, queue.front().app_id) << remark;
+ EXPECT_EQ(kFrom, queue.front().from) << remark;
+ EXPECT_EQ(kByteSize, queue.front().message_byte_size) << remark;
+ EXPECT_EQ(event, queue.front().event) << remark;
+ EXPECT_EQ(details, queue.front().details) << remark;
+ }
+
+ void VerifySendingData(const base::circular_deque<SendingActivity>& queue,
+ const std::string& event,
+ const std::string& details,
+ const std::string& remark) {
+ EXPECT_EQ(kAppId, queue.front().app_id) << remark;
+ EXPECT_EQ(kReceiverId, queue.front().receiver_id) << remark;
+ EXPECT_EQ(kMessageId, queue.front().message_id) << remark;
+ EXPECT_EQ(event, queue.front().event) << remark;
+ EXPECT_EQ(details, queue.front().details) << remark;
+ }
+
+ std::string source_;
+ GCMStatsRecorderImpl recorder_;
+};
+
+GCMStatsRecorderImplTest::GCMStatsRecorderImplTest(){
+}
+
+GCMStatsRecorderImplTest::~GCMStatsRecorderImplTest() {}
+
+void GCMStatsRecorderImplTest::SetUp(){
+ source_ = "s1,s2";
+ recorder_.set_is_recording(true);
+}
+
+TEST_F(GCMStatsRecorderImplTest, StartStopRecordingTest) {
+ EXPECT_TRUE(recorder_.is_recording());
+ recorder_.RecordDataSentToWire(kAppId, kReceiverId, kMessageId, kQueuedSec);
+ VerifyRecordedSendingCount(1);
+ VerifyDataSentToWire("1st call");
+
+ recorder_.set_is_recording(false);
+ EXPECT_FALSE(recorder_.is_recording());
+ recorder_.Clear();
+ VerifyAllActivityQueueEmpty("all cleared");
+
+ // Exercise every recording method below and verify that nothing is recorded.
+ recorder_.RecordCheckinInitiated(kAndroidId);
+ recorder_.RecordCheckinDelayedDueToBackoff(kDelay);
+ recorder_.RecordCheckinSuccess();
+ recorder_.RecordCheckinFailure(kCheckinStatus, true);
+ VerifyAllActivityQueueEmpty("no checkin");
+
+ recorder_.RecordConnectionInitiated(kHost);
+ recorder_.RecordConnectionDelayedDueToBackoff(kDelay);
+ recorder_.RecordConnectionSuccess();
+ recorder_.RecordConnectionFailure(kNetworkError);
+ recorder_.RecordConnectionResetSignaled(kReason);
+ VerifyAllActivityQueueEmpty("no registration");
+
+ recorder_.RecordRegistrationSent(kAppId, kSenderIds);
+ recorder_.RecordRegistrationResponse(kAppId, source_,
+ kRegistrationStatus);
+ recorder_.RecordRegistrationRetryDelayed(kAppId, source_, kDelay, kRetries);
+ recorder_.RecordUnregistrationSent(kAppId, source_);
+ recorder_.RecordUnregistrationResponse(
+ kAppId, source_, kUnregistrationStatus);
+ recorder_.RecordUnregistrationRetryDelayed(kAppId, source_, kDelay, kRetries);
+ VerifyAllActivityQueueEmpty("no unregistration");
+
+ recorder_.RecordDataMessageReceived(kAppId, kFrom, kByteSize,
+ GCMStatsRecorder::DATA_MESSAGE);
+ recorder_.RecordDataMessageReceived(kAppId, kFrom, kByteSize,
+ GCMStatsRecorder::DELETED_MESSAGES);
+ VerifyAllActivityQueueEmpty("no receiving");
+
+ recorder_.RecordDataSentToWire(kAppId, kReceiverId, kMessageId, kQueuedSec);
+ recorder_.RecordNotifySendStatus(kAppId, kReceiverId, kMessageId,
+ kMessageSendStatus, kByteSize, kTTL);
+ recorder_.RecordIncomingSendError(kAppId, kReceiverId, kMessageId);
+ recorder_.RecordDataSentToWire(kAppId, kReceiverId, kMessageId, kQueuedSec);
+ VerifyAllActivityQueueEmpty("no sending");
+}
+
+TEST_F(GCMStatsRecorderImplTest, ClearLogTest) {
+ recorder_.RecordDataSentToWire(kAppId, kReceiverId, kMessageId, kQueuedSec);
+ VerifyRecordedSendingCount(1);
+ VerifyDataSentToWire("1st call");
+
+ recorder_.RecordNotifySendStatus(kAppId, kReceiverId, kMessageId,
+ kMessageSendStatus, kByteSize, kTTL);
+ VerifyRecordedSendingCount(2);
+ VerifyNotifySendStatus("2nd call");
+
+ recorder_.Clear();
+ VerifyRecordedSendingCount(0);
+}
+
+// This test is flaky, see https://crbug.com/1010462
+TEST_F(GCMStatsRecorderImplTest, DISABLED_CheckinTest) {
+ recorder_.RecordCheckinInitiated(kAndroidId);
+ VerifyRecordedCheckinCount(1);
+ VerifyCheckinInitiated("1st call");
+
+ recorder_.RecordCheckinDelayedDueToBackoff(kDelay);
+ VerifyRecordedCheckinCount(2);
+ VerifyCheckinDelayedDueToBackoff("2nd call");
+
+ recorder_.RecordCheckinSuccess();
+ VerifyRecordedCheckinCount(3);
+ VerifyCheckinSuccess("3rd call");
+
+ recorder_.RecordCheckinFailure(kCheckinStatus, true);
+ VerifyRecordedCheckinCount(4);
+ VerifyCheckinFailure("4th call");
+}
+
+TEST_F(GCMStatsRecorderImplTest, ConnectionTest) {
+ recorder_.RecordConnectionInitiated(kHost);
+ VerifyRecordedConnectionCount(1);
+ VerifyConnectionInitiated("1st call");
+
+ recorder_.RecordConnectionDelayedDueToBackoff(kDelay);
+ VerifyRecordedConnectionCount(2);
+ VerifyConnectionDelayedDueToBackoff("2nd call");
+
+ recorder_.RecordConnectionSuccess();
+ VerifyRecordedConnectionCount(3);
+ VerifyConnectionSuccess("3rd call");
+
+ recorder_.RecordConnectionFailure(kNetworkError);
+ VerifyRecordedConnectionCount(4);
+ VerifyConnectionFailure("4th call");
+
+ recorder_.RecordConnectionResetSignaled(kReason);
+ VerifyRecordedConnectionCount(5);
+ VerifyConnectionResetSignaled("5th call");
+}
+
+TEST_F(GCMStatsRecorderImplTest, RegistrationTest) {
+ recorder_.RecordRegistrationSent(kAppId, kSenderIds);
+ VerifyRecordedRegistrationCount(1);
+ VerifyRegistrationSent("1st call");
+
+ recorder_.RecordRegistrationResponse(kAppId, source_,
+ kRegistrationStatus);
+ VerifyRecordedRegistrationCount(2);
+ VerifyRegistrationResponse("2nd call");
+
+ recorder_.RecordRegistrationRetryDelayed(kAppId, source_, kDelay, kRetries);
+ VerifyRecordedRegistrationCount(3);
+ VerifyRegistrationRetryRequested("3rd call");
+
+ recorder_.RecordUnregistrationSent(kAppId, source_);
+ VerifyRecordedRegistrationCount(4);
+ VerifyUnregistrationSent("4th call");
+
+ recorder_.RecordUnregistrationResponse(
+ kAppId, source_, kUnregistrationStatus);
+ VerifyRecordedRegistrationCount(5);
+ VerifyUnregistrationResponse("5th call");
+
+ recorder_.RecordUnregistrationRetryDelayed(kAppId, source_, kDelay, kRetries);
+ VerifyRecordedRegistrationCount(6);
+ VerifyUnregistrationRetryDelayed("6th call");
+}
+
+TEST_F(GCMStatsRecorderImplTest, RecordReceivingTest) {
+ recorder_.RecordConnectionInitiated(std::string());
+ recorder_.RecordConnectionSuccess();
+ recorder_.RecordDataMessageReceived(kAppId, kFrom, kByteSize,
+ GCMStatsRecorder::DATA_MESSAGE);
+ VerifyRecordedReceivingCount(1);
+ VerifyDataMessageReceived("1st call");
+
+ recorder_.RecordDataMessageReceived(kAppId, kFrom, kByteSize,
+ GCMStatsRecorder::DELETED_MESSAGES);
+ VerifyRecordedReceivingCount(2);
+ VerifyDataDeletedMessage("2nd call");
+}
+
+TEST_F(GCMStatsRecorderImplTest, RecordSendingTest) {
+ recorder_.RecordDataSentToWire(kAppId, kReceiverId, kMessageId, kQueuedSec);
+ VerifyRecordedSendingCount(1);
+ VerifyDataSentToWire("1st call");
+
+ recorder_.RecordNotifySendStatus(kAppId, kReceiverId, kMessageId,
+ kMessageSendStatus, kByteSize, kTTL);
+ VerifyRecordedSendingCount(2);
+ VerifyNotifySendStatus("2nd call");
+
+ recorder_.RecordIncomingSendError(kAppId, kReceiverId, kMessageId);
+ VerifyRecordedSendingCount(3);
+ VerifyIncomingSendError("3rd call");
+
+ recorder_.RecordDataSentToWire(kAppId, kReceiverId, kMessageId, kQueuedSec);
+ VerifyRecordedSendingCount(4);
+ VerifyDataSentToWire("4th call");
+}
+
+TEST_F(GCMStatsRecorderImplTest, RecordDecryptionFailureTest) {
+ recorder_.RecordDecryptionFailure(kAppId, kDecryptionResultFailure);
+ VerifyRecordedDecryptionFailureCount(1);
+
+ VerifyRecordedDecryptionFailure("1st call");
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/instance_id/DEPS b/chromium/components/gcm_driver/instance_id/DEPS
new file mode 100644
index 00000000000..2d411161aa4
--- /dev/null
+++ b/chromium/components/gcm_driver/instance_id/DEPS
@@ -0,0 +1,4 @@
+include_rules = [
+ "+components/gcm_driver",
+ "+crypto",
+]
diff --git a/chromium/components/gcm_driver/instance_id/android/javatests/src/org/chromium/components/gcm_driver/instance_id/FakeInstanceIDWithSubtype.java b/chromium/components/gcm_driver/instance_id/android/javatests/src/org/chromium/components/gcm_driver/instance_id/FakeInstanceIDWithSubtype.java
new file mode 100644
index 00000000000..0c467469145
--- /dev/null
+++ b/chromium/components/gcm_driver/instance_id/android/javatests/src/org/chromium/components/gcm_driver/instance_id/FakeInstanceIDWithSubtype.java
@@ -0,0 +1,198 @@
+// Copyright 2016 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.
+
+package org.chromium.components.gcm_driver.instance_id;
+
+import android.os.Looper;
+import android.util.Pair;
+
+import com.google.android.gms.iid.InstanceID;
+
+import org.chromium.base.annotations.CalledByNative;
+import org.chromium.base.annotations.JNINamespace;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+
+/**
+ * Fake for InstanceIDWithSubtype. Doesn't hit the network or filesystem (so instance IDs don't
+ * survive restarts, and sending messages to tokens via the GCM server won't work).
+ */
+@JNINamespace("instance_id")
+public class FakeInstanceIDWithSubtype extends InstanceIDWithSubtype {
+ private String mSubtype;
+ private String mId;
+ private long mCreationTime;
+
+ /** Map from (subtype + ',' + authorizedEntity + ',' + scope) to token. */
+ private Map<String, String> mTokens = new HashMap<>();
+
+ /**
+ * Enable this in all InstanceID tests to use this fake instead of hitting the network/disk.
+ * @return The previous value.
+ */
+ @CalledByNative
+ public static boolean clearDataAndSetEnabled(boolean enable) {
+ synchronized (sSubtypeInstancesLock) {
+ sSubtypeInstances.clear();
+ boolean wasEnabled = sFakeFactoryForTesting != null;
+ if (enable) {
+ sFakeFactoryForTesting = new FakeFactory() {
+ @Override
+ public InstanceIDWithSubtype create(String subtype) {
+ return new FakeInstanceIDWithSubtype(subtype);
+ }
+ };
+ } else {
+ sFakeFactoryForTesting = null;
+ }
+ return wasEnabled;
+ }
+ }
+
+ /**
+ * If exactly one instance of InstanceID exists, and it has exactly one token, this returns
+ * the subtype of the InstanceID and the authorizedEntity of the token. Otherwise it throws.
+ * If a test fails with no InstanceID or no tokens, it probably means subscribing failed, or
+ * that the test subscribed in the wrong way (e.g. a GCM registration rather than an InstanceID
+ * token). If a test fails with too many InstanceIDs/tokens, the test subscribed too many times.
+ */
+ public static Pair<String, String> getSubtypeAndAuthorizedEntityOfOnlyToken() {
+ synchronized (sSubtypeInstancesLock) {
+ if (sSubtypeInstances.size() != 1) {
+ throw new IllegalStateException("Expected exactly one InstanceID, but there are "
+ + sSubtypeInstances.size());
+ }
+ final String subType = sSubtypeInstances.values().iterator().next().getSubtype();
+ return Pair.create(subType, getAuthorizedEntityForSubtype(subType));
+ }
+ }
+
+ /**
+ * If an instanceID exists for subtype, and it has exactly one token, this returns
+ * the authorizedEntity of the token. Otherwise it throws.
+ */
+ public static String getAuthorizedEntityForSubtype(String subtype) {
+ synchronized (sSubtypeInstancesLock) {
+ FakeInstanceIDWithSubtype iid =
+ (FakeInstanceIDWithSubtype) sSubtypeInstances.get(subtype);
+ if (iid == null) {
+ throw new IllegalStateException("No subtype instance found for " + subtype);
+ }
+ if (iid.mTokens.size() != 1) {
+ throw new IllegalStateException(
+ "Expected exactly one token, but there are " + iid.mTokens.size());
+ }
+ return iid.mTokens.keySet().iterator().next().split(",", 3)[1];
+ }
+ }
+
+ private FakeInstanceIDWithSubtype(String subtype) {
+ super(null);
+ mSubtype = subtype;
+
+ // The first call to InstanceIDWithSubtype.getInstance calls InstanceID.getInstance which
+ // triggers a strict mode violation if it's called on the main thread, by reading from
+ // SharedPreferences. Since we can't override those static methods to simulate the strict
+ // mode violation in tests, check the thread here (which is only called from getInstance).
+ if (Looper.getMainLooper() == Looper.myLooper()) {
+ throw new AssertionError(InstanceID.ERROR_MAIN_THREAD);
+ }
+ }
+
+ @Override
+ public String getSubtype() {
+ return mSubtype;
+ }
+
+ @Override
+ public String getId() {
+ // InstanceID.getId sometimes triggers a strict mode violation if it's called on the main
+ // thread, by reading from SharedPreferences.
+ if (Looper.getMainLooper() == Looper.myLooper()) {
+ throw new AssertionError(InstanceID.ERROR_MAIN_THREAD);
+ }
+
+ if (mId == null) {
+ mCreationTime = System.currentTimeMillis();
+ mId = randomBase64(11 /* length */);
+ }
+ return mId;
+ }
+
+ @Override
+ public long getCreationTime() {
+ // InstanceID.getCreationTime sometimes triggers a strict mode violation if it's called on
+ // the main thread, by reading from SharedPreferences.
+ if (Looper.getMainLooper() == Looper.myLooper()) {
+ throw new AssertionError(InstanceID.ERROR_MAIN_THREAD);
+ }
+
+ return mCreationTime;
+ }
+
+ @Override
+ public String getToken(String authorizedEntity, String scope) throws IOException {
+ // InstanceID.getToken enforces this.
+ if (Looper.getMainLooper() == Looper.myLooper()) {
+ throw new IOException(InstanceID.ERROR_MAIN_THREAD);
+ }
+
+ String key = getSubtype() + ',' + authorizedEntity + ',' + scope;
+ String token = mTokens.get(key);
+ if (token == null) {
+ getId();
+ token = mId + ':' + randomBase64(140 /* length */);
+ mTokens.put(key, token);
+ }
+ return token;
+ }
+
+ @Override
+ public void deleteToken(String authorizedEntity, String scope) throws IOException {
+ // InstanceID.deleteToken enforces this.
+ if (Looper.getMainLooper() == Looper.myLooper()) {
+ throw new IOException(InstanceID.ERROR_MAIN_THREAD);
+ }
+
+ String key = getSubtype() + ',' + authorizedEntity + ',' + scope;
+ mTokens.remove(key);
+ // Calling deleteToken causes ID to be generated; can be observed though getCreationTime.
+ getId();
+ }
+
+ @Override
+ public void deleteInstanceID() throws IOException {
+ synchronized (sSubtypeInstancesLock) {
+ sSubtypeInstances.remove(getSubtype());
+
+ // InstanceID.deleteInstanceID calls InstanceID.deleteToken which enforces this.
+ if (Looper.getMainLooper() == Looper.myLooper()) {
+ throw new IOException(InstanceID.ERROR_MAIN_THREAD);
+ }
+
+ mTokens.clear();
+ mCreationTime = 0;
+ mId = null;
+ }
+ }
+
+ /** Returns a random base64url encoded string. */
+ private static String randomBase64(int encodedLength) {
+ // It would probably make more sense for this method to produce fixed-length plaintext,
+ // rather than fixed-length encodings that correspond to variable-length plaintext.
+ // But the added randomness helps avoid us depending on the length of tokens GCM gives us.
+ final String base64urlAlphabet =
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
+ Random random = new Random();
+ StringBuilder sb = new StringBuilder(encodedLength);
+ for (int i = 0; i < encodedLength; i++) {
+ int index = random.nextInt(base64urlAlphabet.length());
+ sb.append(base64urlAlphabet.charAt(index));
+ }
+ return sb.toString();
+ }
+}
diff --git a/chromium/components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.cc b/chromium/components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.cc
new file mode 100644
index 00000000000..a33c49aeb2f
--- /dev/null
+++ b/chromium/components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.cc
@@ -0,0 +1,121 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.h"
+
+#include "base/bind.h"
+#include "base/location.h"
+#include "base/rand_util.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_util.h"
+#include "base/task/single_thread_task_runner.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "components/gcm_driver/gcm_client.h"
+
+namespace instance_id {
+
+FakeGCMDriverForInstanceID::FakeGCMDriverForInstanceID()
+ : gcm::FakeGCMDriver(base::ThreadTaskRunnerHandle::Get()) {}
+
+FakeGCMDriverForInstanceID::FakeGCMDriverForInstanceID(
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner)
+ : FakeGCMDriver(blocking_task_runner) {}
+
+FakeGCMDriverForInstanceID::~FakeGCMDriverForInstanceID() {
+}
+
+gcm::InstanceIDHandler*
+FakeGCMDriverForInstanceID::GetInstanceIDHandlerInternal() {
+ return this;
+}
+
+void FakeGCMDriverForInstanceID::AddInstanceIDData(
+ const std::string& app_id,
+ const std::string& instance_id,
+ const std::string& extra_data) {
+ instance_id_data_[app_id] = std::make_pair(instance_id, extra_data);
+}
+
+void FakeGCMDriverForInstanceID::RemoveInstanceIDData(
+ const std::string& app_id) {
+ instance_id_data_.erase(app_id);
+}
+
+void FakeGCMDriverForInstanceID::GetInstanceIDData(
+ const std::string& app_id,
+ GetInstanceIDDataCallback callback) {
+ auto iter = instance_id_data_.find(app_id);
+ std::string instance_id;
+ std::string extra_data;
+ if (iter != instance_id_data_.end()) {
+ instance_id = iter->second.first;
+ extra_data = iter->second.second;
+ }
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::BindOnce(std::move(callback), instance_id, extra_data));
+}
+
+void FakeGCMDriverForInstanceID::GetToken(
+ const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ base::TimeDelta time_to_live,
+ GetTokenCallback callback) {
+ std::string key = app_id + authorized_entity + scope;
+ auto iter = tokens_.find(key);
+ std::string token;
+ if (iter != tokens_.end()) {
+ token = iter->second;
+ } else {
+ token = base::NumberToString(base::RandUint64());
+ tokens_[key] = token;
+ }
+
+ last_gettoken_app_id_ = app_id;
+ last_gettoken_authorized_entity_ = authorized_entity;
+
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(std::move(callback), token, gcm::GCMClient::SUCCESS));
+}
+
+void FakeGCMDriverForInstanceID::ValidateToken(
+ const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ const std::string& token,
+ ValidateTokenCallback callback) {
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::BindOnce(std::move(callback), true /* is_valid */));
+}
+
+void FakeGCMDriverForInstanceID::DeleteToken(
+ const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ DeleteTokenCallback callback) {
+ std::string key_prefix = app_id;
+
+ // Calls to InstanceID::DeleteID() will end up deleting the token for a given
+ // |app_id| with both |authorized_entity| and |scope| set to "*", meaning that
+ // all data has to be deleted. Do a prefix search to emulate this behaviour.
+ if (authorized_entity != "*")
+ key_prefix += authorized_entity;
+ if (scope != "*")
+ key_prefix += scope;
+
+ for (auto iter = tokens_.begin(); iter != tokens_.end();) {
+ if (base::StartsWith(iter->first, key_prefix, base::CompareCase::SENSITIVE))
+ iter = tokens_.erase(iter);
+ else
+ iter++;
+ }
+
+ last_deletetoken_app_id_ = app_id;
+
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::BindOnce(std::move(callback), gcm::GCMClient::SUCCESS));
+}
+
+} // namespace instance_id
diff --git a/chromium/components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.h b/chromium/components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.h
new file mode 100644
index 00000000000..2f133802825
--- /dev/null
+++ b/chromium/components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.h
@@ -0,0 +1,80 @@
+// Copyright 2015 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 COMPONENTS_GCM_DRIVER_INSTANCE_ID_FAKE_GCM_DRIVER_FOR_INSTANCE_ID_H_
+#define COMPONENTS_GCM_DRIVER_INSTANCE_ID_FAKE_GCM_DRIVER_FOR_INSTANCE_ID_H_
+
+#include <map>
+#include <string>
+#include <utility>
+
+#include "base/compiler_specific.h"
+#include "components/gcm_driver/fake_gcm_driver.h"
+
+namespace base {
+class SequencedTaskRunner;
+}
+
+namespace instance_id {
+
+class FakeGCMDriverForInstanceID : public gcm::FakeGCMDriver,
+ protected gcm::InstanceIDHandler {
+ public:
+ FakeGCMDriverForInstanceID();
+ explicit FakeGCMDriverForInstanceID(
+ const scoped_refptr<base::SequencedTaskRunner>& blocking_task_runner);
+
+ FakeGCMDriverForInstanceID(const FakeGCMDriverForInstanceID&) = delete;
+ FakeGCMDriverForInstanceID& operator=(const FakeGCMDriverForInstanceID&) =
+ delete;
+
+ ~FakeGCMDriverForInstanceID() override;
+
+ // FakeGCMDriver overrides:
+ gcm::InstanceIDHandler* GetInstanceIDHandlerInternal() override;
+
+ const std::string& last_gettoken_app_id() const {
+ return last_gettoken_app_id_;
+ }
+ const std::string& last_gettoken_authorized_entity() const {
+ return last_gettoken_authorized_entity_;
+ }
+ const std::string& last_deletetoken_app_id() const {
+ return last_deletetoken_app_id_;
+ }
+
+ protected:
+ // InstanceIDHandler overrides:
+ void GetToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ base::TimeDelta time_to_live,
+ GetTokenCallback callback) override;
+ void ValidateToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ const std::string& token,
+ ValidateTokenCallback callback) override;
+ void DeleteToken(const std::string& app_id,
+ const std::string& authorized_entity,
+ const std::string& scope,
+ DeleteTokenCallback callback) override;
+ void AddInstanceIDData(const std::string& app_id,
+ const std::string& instance_id,
+ const std::string& extra_data) override;
+ void RemoveInstanceIDData(const std::string& app_id) override;
+ void GetInstanceIDData(const std::string& app_id,
+ GetInstanceIDDataCallback callback) override;
+
+ private:
+ std::map<std::string, std::pair<std::string, std::string>> instance_id_data_;
+ std::map<std::string, std::string> tokens_;
+ std::string last_gettoken_app_id_;
+ std::string last_gettoken_authorized_entity_;
+ std::string last_deletetoken_app_id_;
+};
+
+} // namespace instance_id
+
+#endif // COMPONENTS_GCM_DRIVER_INSTANCE_ID_FAKE_GCM_DRIVER_FOR_INSTANCE_ID_H_
diff --git a/chromium/components/gcm_driver/instance_id/instance_id.cc b/chromium/components/gcm_driver/instance_id/instance_id.cc
new file mode 100644
index 00000000000..6aca1265f76
--- /dev/null
+++ b/chromium/components/gcm_driver/instance_id/instance_id.cc
@@ -0,0 +1,58 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/instance_id/instance_id.h"
+
+#include "base/bind.h"
+#include "components/gcm_driver/gcm_driver.h"
+
+namespace instance_id {
+
+// A common use case for InstanceID tokens is to authorize and route push
+// messages sent via Google Cloud Messaging (replacing the earlier registration
+// IDs). To get such a GCM-enabled token, pass this scope to getToken.
+// Must match Java GoogleCloudMessaging.INSTANCE_ID_SCOPE.
+const char kGCMScope[] = "GCM";
+
+InstanceID::InstanceID(const std::string& app_id, gcm::GCMDriver* gcm_driver)
+ : gcm_driver_(gcm_driver), app_id_(app_id) {}
+
+InstanceID::~InstanceID() {}
+
+void InstanceID::GetEncryptionInfo(const std::string& authorized_entity,
+ GetEncryptionInfoCallback callback) {
+ gcm_driver_->GetEncryptionProviderInternal()->GetEncryptionInfo(
+ app_id_, authorized_entity, std::move(callback));
+}
+
+void InstanceID::DeleteToken(const std::string& authorized_entity,
+ const std::string& scope,
+ DeleteTokenCallback callback) {
+ // Tokens with GCM scope act as Google Cloud Messaging registrations, so may
+ // have associated encryption information in the GCMKeyStore. This needs to be
+ // cleared when the token is deleted.
+ DeleteTokenCallback wrapped_callback =
+ scope == kGCMScope
+ ? base::BindOnce(&InstanceID::DidDelete,
+ weak_ptr_factory_.GetWeakPtr(), authorized_entity,
+ std::move(callback))
+ : std::move(callback);
+ DeleteTokenImpl(authorized_entity, scope, std::move(wrapped_callback));
+}
+
+void InstanceID::DeleteID(DeleteIDCallback callback) {
+ // Use "*" as authorized_entity to remove any encryption info for all tokens.
+ DeleteIDImpl(
+ base::BindOnce(&InstanceID::DidDelete, weak_ptr_factory_.GetWeakPtr(),
+ "*" /* authorized_entity */, std::move(callback)));
+}
+
+void InstanceID::DidDelete(const std::string& authorized_entity,
+ base::OnceCallback<void(Result result)> callback,
+ Result result) {
+ gcm_driver_->GetEncryptionProviderInternal()->RemoveEncryptionInfo(
+ app_id_, authorized_entity, base::BindOnce(std::move(callback), result));
+}
+
+} // namespace instance_id
diff --git a/chromium/components/gcm_driver/instance_id/instance_id.h b/chromium/components/gcm_driver/instance_id/instance_id.h
new file mode 100644
index 00000000000..01c19c46883
--- /dev/null
+++ b/chromium/components/gcm_driver/instance_id/instance_id.h
@@ -0,0 +1,182 @@
+// Copyright 2015 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 COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_H_
+#define COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_H_
+
+#include <memory>
+#include <set>
+#include <string>
+
+#include "base/callback.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/time/time.h"
+
+namespace gcm {
+class GCMDriver;
+} // namespace gcm
+
+namespace instance_id {
+
+extern const char kGCMScope[];
+
+// Encapsulates Instance ID functionalities that need to be implemented for
+// different platforms. One instance is created per application. Life of
+// Instance ID is managed by the InstanceIDDriver.
+//
+// Create instances of this class by calling |InstanceIDDriver::GetInstanceID|.
+class InstanceID {
+ public:
+ // Used in UMA. Can add enum values, but never renumber or delete and reuse.
+ enum Result {
+ // Successful operation.
+ SUCCESS = 0,
+ // Invalid parameter.
+ INVALID_PARAMETER = 1,
+ // Instance ID is disabled.
+ DISABLED = 2,
+ // Previous asynchronous operation is still pending to finish.
+ ASYNC_OPERATION_PENDING = 3,
+ // Network socket error.
+ NETWORK_ERROR = 4,
+ // Problem at the server.
+ SERVER_ERROR = 5,
+ // 6 is omitted, in case we ever merge this enum with GCMClient::Result.
+ // Other errors.
+ UNKNOWN_ERROR = 7,
+
+ // Used for UMA. Keep kMaxValue up to date and sync with histograms.xml.
+ kMaxValue = UNKNOWN_ERROR
+ };
+
+ // Flags to be used to create a token. These might be platform specific.
+ // GENERATED_JAVA_ENUM_PACKAGE: org.chromium.components.gcm_driver
+ // GENERATED_JAVA_CLASS_NAME_OVERRIDE: InstanceIDFlags
+ enum class Flags {
+ // Whether delivery of received messages should be deferred until there is a
+ // visible activity. Only applicable for Android.
+ kIsLazy = 1 << 0,
+ // Whether delivery of received messages should bypass the background task
+ // scheduler. Only applicable for high priority messages on Android.
+ kBypassScheduler = 1 << 1,
+ };
+
+ // Asynchronous callbacks. Must not synchronously delete |this| (using
+ // InstanceIDDriver::RemoveInstanceID).
+ using GetIDCallback = base::OnceCallback<void(const std::string& id)>;
+ using GetCreationTimeCallback =
+ base::OnceCallback<void(const base::Time& creation_time)>;
+ using GetTokenCallback =
+ base::OnceCallback<void(const std::string& token, Result result)>;
+ using ValidateTokenCallback = base::OnceCallback<void(bool is_valid)>;
+ using GetEncryptionInfoCallback =
+ base::OnceCallback<void(std::string p256dh, std::string auth_secret)>;
+ using DeleteTokenCallback = base::OnceCallback<void(Result result)>;
+ using DeleteIDCallback = base::OnceCallback<void(Result result)>;
+
+ static const int kInstanceIDByteLength = 8;
+
+ // Creator. Should only be used by InstanceIDDriver::GetInstanceID.
+ // |app_id|: identifies the application that uses the Instance ID.
+ // |handler|: provides the GCM functionality needed to support Instance ID.
+ // Must outlive this class. On Android, this can be null instead.
+ static std::unique_ptr<InstanceID> CreateInternal(const std::string& app_id,
+ gcm::GCMDriver* gcm_driver);
+
+ InstanceID(const InstanceID&) = delete;
+ InstanceID& operator=(const InstanceID&) = delete;
+
+ virtual ~InstanceID();
+
+ // Returns the Instance ID.
+ virtual void GetID(GetIDCallback callback) = 0;
+
+ // Returns the time when the Instance ID has been generated.
+ virtual void GetCreationTime(GetCreationTimeCallback callback) = 0;
+
+ // Retrieves a token that allows the authorized entity to access the service
+ // defined as "scope". This may cause network requests but the result is
+ // cached on disk for up to a week. Token validity will be checked
+ // automatically. Thus you should not store tokens for long periods yourself,
+ // instead call this function each time it's needed.
+ //
+ // To receive messages, register an |AppIdHandler| on |gcm_driver()|.
+ //
+ // |authorized_entity|: identifies the entity that is authorized to access
+ // resources associated with this Instance ID. It can be
+ // another Instance ID or a numeric project ID.
+ // |scope|: identifies authorized actions that the authorized entity can take.
+ // E.g. for sending GCM messages, "GCM" scope should be used.
+ // |time_to_live|: TTL of retrieved token, unlimited if zero value passed.
+ // |flags|: Flags used to create this token.
+ // |callback|: to be called once the asynchronous operation is done.
+ virtual void GetToken(const std::string& authorized_entity,
+ const std::string& scope,
+ base::TimeDelta time_to_live,
+ std::set<Flags> flags,
+ GetTokenCallback callback) = 0;
+
+ // Checks that the provided |token| matches the stored token for (|app_id()|,
+ // |authorized_entity|, |scope|). If you follow the guidance for |GetToken|,
+ // and call that function each time you need the token, then you will not
+ // need to use this function.
+ virtual void ValidateToken(const std::string& authorized_entity,
+ const std::string& scope,
+ const std::string& token,
+ ValidateTokenCallback callback) = 0;
+
+ // Get the public encryption key and authentication secret associated with a
+ // GCM-scoped token. If encryption info is not yet associated, it will be
+ // created.
+ // |authorized_entity|: the authorized entity passed when obtaining the token.
+ // |callback|: to be called once the asynchronous operation is done.
+ virtual void GetEncryptionInfo(const std::string& authorized_entity,
+ GetEncryptionInfoCallback callback);
+
+ // Revokes a granted token.
+ // |authorized_entity|: the authorized entity passed when obtaining the token.
+ // |scope|: the scope that was passed when obtaining the token.
+ // |callback|: to be called once the asynchronous operation is done.
+ virtual void DeleteToken(const std::string& authorized_entity,
+ const std::string& scope,
+ DeleteTokenCallback callback);
+
+ // Resets the app instance identifier and revokes all tokens associated with
+ // it.
+ // |callback|: to be called once the asynchronous operation is done.
+ void DeleteID(DeleteIDCallback callback);
+
+ std::string app_id() const { return app_id_; }
+
+ gcm::GCMDriver* gcm_driver() { return gcm_driver_; }
+
+ protected:
+ InstanceID(const std::string& app_id, gcm::GCMDriver* gcm_driver);
+
+ // Platform-specific implementations.
+ virtual void DeleteTokenImpl(const std::string& authorized_entity,
+ const std::string& scope,
+ DeleteTokenCallback callback) = 0;
+ virtual void DeleteIDImpl(DeleteIDCallback callback) = 0;
+
+ void NotifyTokenRefresh(bool update_id);
+
+ private:
+ void DidDelete(const std::string& authorized_entity,
+ base::OnceCallback<void(Result result)> callback,
+ Result result);
+
+ // Owned by GCMProfileServiceFactory, which is a dependency of
+ // InstanceIDProfileServiceFactory, which owns this.
+ raw_ptr<gcm::GCMDriver> gcm_driver_;
+
+ std::string app_id_;
+
+ base::WeakPtrFactory<InstanceID> weak_ptr_factory_{this};
+};
+
+} // namespace instance_id
+
+#endif // COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_H_
diff --git a/chromium/components/gcm_driver/instance_id/instance_id_android.cc b/chromium/components/gcm_driver/instance_id/instance_id_android.cc
new file mode 100644
index 00000000000..ace47d9eeef
--- /dev/null
+++ b/chromium/components/gcm_driver/instance_id/instance_id_android.cc
@@ -0,0 +1,229 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/instance_id/instance_id_android.h"
+
+#include <stdint.h>
+#include <memory>
+#include <numeric>
+
+#include "base/android/jni_android.h"
+#include "base/android/jni_string.h"
+#include "base/bind.h"
+#include "base/location.h"
+#include "base/logging.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/time/time.h"
+#include "components/gcm_driver/instance_id/android/jni_headers/InstanceIDBridge_jni.h"
+
+using base::android::AttachCurrentThread;
+using base::android::ConvertJavaStringToUTF8;
+using base::android::ConvertUTF8ToJavaString;
+
+namespace instance_id {
+
+InstanceIDAndroid::ScopedBlockOnAsyncTasksForTesting::
+ ScopedBlockOnAsyncTasksForTesting() {
+ JNIEnv* env = AttachCurrentThread();
+ previous_value_ =
+ Java_InstanceIDBridge_setBlockOnAsyncTasksForTesting(env, true);
+}
+
+InstanceIDAndroid::ScopedBlockOnAsyncTasksForTesting::
+ ~ScopedBlockOnAsyncTasksForTesting() {
+ JNIEnv* env = AttachCurrentThread();
+ Java_InstanceIDBridge_setBlockOnAsyncTasksForTesting(env, previous_value_);
+}
+
+// static
+std::unique_ptr<InstanceID> InstanceID::CreateInternal(
+ const std::string& app_id,
+ gcm::GCMDriver* gcm_driver) {
+ return std::make_unique<InstanceIDAndroid>(app_id, gcm_driver);
+}
+
+InstanceIDAndroid::InstanceIDAndroid(const std::string& app_id,
+ gcm::GCMDriver* gcm_driver)
+ : InstanceID(app_id, gcm_driver) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ DCHECK(!app_id.empty()) << "Empty app_id is not supported";
+ // The |app_id| is stored in GCM's category field by the desktop InstanceID
+ // implementation, but because the category is reserved for the app's package
+ // name on Android the subtype field is used instead.
+ std::string subtype = app_id;
+
+ JNIEnv* env = AttachCurrentThread();
+ java_ref_.Reset(
+ Java_InstanceIDBridge_create(env, reinterpret_cast<intptr_t>(this),
+ ConvertUTF8ToJavaString(env, subtype)));
+}
+
+InstanceIDAndroid::~InstanceIDAndroid() {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ JNIEnv* env = AttachCurrentThread();
+ Java_InstanceIDBridge_destroy(env, java_ref_);
+}
+
+void InstanceIDAndroid::GetID(GetIDCallback callback) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ int32_t request_id = get_id_callbacks_.Add(
+ std::make_unique<GetIDCallback>(std::move(callback)));
+
+ JNIEnv* env = AttachCurrentThread();
+ Java_InstanceIDBridge_getId(env, java_ref_, request_id);
+}
+
+void InstanceIDAndroid::GetCreationTime(GetCreationTimeCallback callback) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ int32_t request_id = get_creation_time_callbacks_.Add(
+ std::make_unique<GetCreationTimeCallback>(std::move(callback)));
+
+ JNIEnv* env = AttachCurrentThread();
+ Java_InstanceIDBridge_getCreationTime(env, java_ref_, request_id);
+}
+
+void InstanceIDAndroid::GetToken(
+ const std::string& authorized_entity,
+ const std::string& scope,
+ base::TimeDelta time_to_live,
+ std::set<Flags> flags,
+ GetTokenCallback callback) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ if (!time_to_live.is_zero()) {
+ LOG(WARNING) << "Non-zero TTL requested for InstanceID token, while TTLs"
+ " are not supported by Android Firebase IID API.";
+ }
+
+ int32_t request_id = get_token_callbacks_.Add(
+ std::make_unique<GetTokenCallback>(std::move(callback)));
+
+ int java_flags = std::accumulate(
+ flags.begin(), flags.end(), 0,
+ [](int sum, Flags flag) { return sum + static_cast<int>(flag); });
+
+ JNIEnv* env = AttachCurrentThread();
+ Java_InstanceIDBridge_getToken(
+ env, java_ref_, request_id,
+ ConvertUTF8ToJavaString(env, authorized_entity),
+ ConvertUTF8ToJavaString(env, scope), java_flags);
+}
+
+void InstanceIDAndroid::ValidateToken(const std::string& authorized_entity,
+ const std::string& scope,
+ const std::string& token,
+ ValidateTokenCallback callback) {
+ // gcm_driver doesn't store tokens on Android, so assume it's valid.
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::BindOnce(std::move(callback), true /* is_valid */));
+}
+
+void InstanceIDAndroid::DeleteTokenImpl(const std::string& authorized_entity,
+ const std::string& scope,
+ DeleteTokenCallback callback) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ int32_t request_id = delete_token_callbacks_.Add(
+ std::make_unique<DeleteTokenCallback>(std::move(callback)));
+
+ JNIEnv* env = AttachCurrentThread();
+ Java_InstanceIDBridge_deleteToken(
+ env, java_ref_, request_id,
+ ConvertUTF8ToJavaString(env, authorized_entity),
+ ConvertUTF8ToJavaString(env, scope));
+}
+
+void InstanceIDAndroid::DeleteIDImpl(DeleteIDCallback callback) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ int32_t request_id = delete_id_callbacks_.Add(
+ std::make_unique<DeleteIDCallback>(std::move(callback)));
+
+ JNIEnv* env = AttachCurrentThread();
+ Java_InstanceIDBridge_deleteInstanceID(env, java_ref_, request_id);
+}
+
+void InstanceIDAndroid::DidGetID(
+ JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj,
+ jint request_id,
+ const base::android::JavaParamRef<jstring>& jid) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ GetIDCallback* callback = get_id_callbacks_.Lookup(request_id);
+ DCHECK(callback);
+ std::move(*callback).Run(ConvertJavaStringToUTF8(jid));
+ get_id_callbacks_.Remove(request_id);
+}
+
+void InstanceIDAndroid::DidGetCreationTime(
+ JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj,
+ jint request_id,
+ jlong creation_time_unix_ms) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ base::Time creation_time;
+ // If the InstanceID's getId, getToken and deleteToken methods have never been
+ // called, or deleteInstanceID has cleared it since, creation time will be 0.
+ if (creation_time_unix_ms) {
+ creation_time =
+ base::Time::UnixEpoch() + base::Milliseconds(creation_time_unix_ms);
+ }
+
+ GetCreationTimeCallback* callback =
+ get_creation_time_callbacks_.Lookup(request_id);
+ DCHECK(callback);
+ std::move(*callback).Run(creation_time);
+ get_creation_time_callbacks_.Remove(request_id);
+}
+
+void InstanceIDAndroid::DidGetToken(
+ JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj,
+ jint request_id,
+ const base::android::JavaParamRef<jstring>& jtoken) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ GetTokenCallback* callback = get_token_callbacks_.Lookup(request_id);
+ DCHECK(callback);
+ std::string token = ConvertJavaStringToUTF8(jtoken);
+ std::move(*callback).Run(
+ token, token.empty() ? InstanceID::UNKNOWN_ERROR : InstanceID::SUCCESS);
+ get_token_callbacks_.Remove(request_id);
+}
+
+void InstanceIDAndroid::DidDeleteToken(
+ JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj,
+ jint request_id,
+ jboolean success) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ DeleteTokenCallback* callback = delete_token_callbacks_.Lookup(request_id);
+ DCHECK(callback);
+ std::move(*callback).Run(success ? InstanceID::SUCCESS
+ : InstanceID::UNKNOWN_ERROR);
+ delete_token_callbacks_.Remove(request_id);
+}
+
+void InstanceIDAndroid::DidDeleteID(
+ JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj,
+ jint request_id,
+ jboolean success) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ DeleteIDCallback* callback = delete_id_callbacks_.Lookup(request_id);
+ DCHECK(callback);
+ std::move(*callback).Run(success ? InstanceID::SUCCESS
+ : InstanceID::UNKNOWN_ERROR);
+ delete_id_callbacks_.Remove(request_id);
+}
+
+} // namespace instance_id
diff --git a/chromium/components/gcm_driver/instance_id/instance_id_android.h b/chromium/components/gcm_driver/instance_id/instance_id_android.h
new file mode 100644
index 00000000000..40b77244059
--- /dev/null
+++ b/chromium/components/gcm_driver/instance_id/instance_id_android.h
@@ -0,0 +1,104 @@
+// Copyright 2015 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 COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_ANDROID_H_
+#define COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_ANDROID_H_
+
+#include <jni.h>
+
+#include <string>
+
+#include "base/android/scoped_java_ref.h"
+#include "base/callback.h"
+#include "base/compiler_specific.h"
+#include "base/containers/id_map.h"
+#include "base/threading/thread_checker.h"
+#include "base/time/time.h"
+#include "components/gcm_driver/instance_id/instance_id.h"
+
+namespace instance_id {
+
+// InstanceID implementation for Android.
+class InstanceIDAndroid : public InstanceID {
+ public:
+ // Tests depending on InstanceID that run without a nested Java message loop
+ // must use this. Operations that would normally be asynchronous will instead
+ // block the UI thread.
+ class ScopedBlockOnAsyncTasksForTesting {
+ public:
+ ScopedBlockOnAsyncTasksForTesting();
+
+ ScopedBlockOnAsyncTasksForTesting(
+ const ScopedBlockOnAsyncTasksForTesting&) = delete;
+ ScopedBlockOnAsyncTasksForTesting& operator=(
+ const ScopedBlockOnAsyncTasksForTesting&) = delete;
+
+ ~ScopedBlockOnAsyncTasksForTesting();
+
+ private:
+ bool previous_value_;
+ };
+
+ InstanceIDAndroid(const std::string& app_id, gcm::GCMDriver* gcm_driver);
+
+ InstanceIDAndroid(const InstanceIDAndroid&) = delete;
+ InstanceIDAndroid& operator=(const InstanceIDAndroid&) = delete;
+
+ ~InstanceIDAndroid() override;
+
+ // InstanceID implementation:
+ void GetID(GetIDCallback callback) override;
+ void GetCreationTime(GetCreationTimeCallback callback) override;
+ void GetToken(const std::string& audience,
+ const std::string& scope,
+ base::TimeDelta time_to_live,
+ std::set<Flags> flags,
+ GetTokenCallback callback) override;
+ void ValidateToken(const std::string& authorized_entity,
+ const std::string& scope,
+ const std::string& token,
+ ValidateTokenCallback callback) override;
+ void DeleteTokenImpl(const std::string& audience,
+ const std::string& scope,
+ DeleteTokenCallback callback) override;
+ void DeleteIDImpl(DeleteIDCallback callback) override;
+
+ // Methods called from Java via JNI:
+ void DidGetID(JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj,
+ jint request_id,
+ const base::android::JavaParamRef<jstring>& jid);
+ void DidGetCreationTime(JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj,
+ jint request_id,
+ jlong creation_time_unix_ms);
+ void DidGetToken(JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj,
+ jint request_id,
+ const base::android::JavaParamRef<jstring>& jtoken);
+ void DidDeleteToken(JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj,
+ jint request_id,
+ jboolean success);
+ void DidDeleteID(JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj,
+ jint request_id,
+ jboolean success);
+
+ private:
+ base::android::ScopedJavaGlobalRef<jobject> java_ref_;
+
+ base::IDMap<std::unique_ptr<GetIDCallback>> get_id_callbacks_;
+ base::IDMap<std::unique_ptr<GetCreationTimeCallback>>
+ get_creation_time_callbacks_;
+ base::IDMap<std::unique_ptr<GetTokenCallback>> get_token_callbacks_;
+ base::IDMap<std::unique_ptr<DeleteTokenCallback>> delete_token_callbacks_;
+ base::IDMap<std::unique_ptr<DeleteIDCallback>> delete_id_callbacks_;
+
+ base::ThreadChecker thread_checker_;
+};
+
+} // namespace instance_id
+
+#endif // COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_ANDROID_H_
diff --git a/chromium/components/gcm_driver/instance_id/instance_id_driver.cc b/chromium/components/gcm_driver/instance_id/instance_id_driver.cc
new file mode 100644
index 00000000000..52756898a77
--- /dev/null
+++ b/chromium/components/gcm_driver/instance_id/instance_id_driver.cc
@@ -0,0 +1,40 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/instance_id/instance_id_driver.h"
+
+#include "build/build_config.h"
+#include "components/gcm_driver/gcm_driver.h"
+#include "components/gcm_driver/instance_id/instance_id.h"
+
+namespace instance_id {
+
+InstanceIDDriver::InstanceIDDriver(gcm::GCMDriver* gcm_driver)
+ : gcm_driver_(gcm_driver) {
+}
+
+InstanceIDDriver::~InstanceIDDriver() {
+}
+
+InstanceID* InstanceIDDriver::GetInstanceID(const std::string& app_id) {
+ auto iter = instance_id_map_.find(app_id);
+ if (iter != instance_id_map_.end())
+ return iter->second.get();
+
+ std::unique_ptr<InstanceID> instance_id =
+ InstanceID::CreateInternal(app_id, gcm_driver_);
+ InstanceID* instance_id_ptr = instance_id.get();
+ instance_id_map_.insert(std::make_pair(app_id, std::move(instance_id)));
+ return instance_id_ptr;
+}
+
+void InstanceIDDriver::RemoveInstanceID(const std::string& app_id) {
+ instance_id_map_.erase(app_id);
+}
+
+bool InstanceIDDriver::ExistsInstanceID(const std::string& app_id) const {
+ return instance_id_map_.find(app_id) != instance_id_map_.end();
+}
+
+} // namespace instance_id
diff --git a/chromium/components/gcm_driver/instance_id/instance_id_driver.h b/chromium/components/gcm_driver/instance_id/instance_id_driver.h
new file mode 100644
index 00000000000..8fe328fa22f
--- /dev/null
+++ b/chromium/components/gcm_driver/instance_id/instance_id_driver.h
@@ -0,0 +1,58 @@
+// Copyright 2015 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 COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_DRIVER_H_
+#define COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_DRIVER_H_
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include "base/memory/raw_ptr.h"
+
+namespace gcm {
+class GCMDriver;
+} // namespace gcm
+
+namespace instance_id {
+
+class InstanceID;
+
+// Bridge between Instance ID users in Chrome and the platform-specific
+// implementation.
+//
+// Create instances of this class with |InstanceIDProfileServiceFactory|.
+class InstanceIDDriver {
+ public:
+ explicit InstanceIDDriver(gcm::GCMDriver* gcm_driver);
+
+ InstanceIDDriver(const InstanceIDDriver&) = delete;
+ InstanceIDDriver& operator=(const InstanceIDDriver&) = delete;
+
+ virtual ~InstanceIDDriver();
+
+ // Returns the InstanceID that provides the Instance ID service for the given
+ // application. The lifetime of the InstanceID will be managed by this class.
+ // App IDs are arbitrary strings that typically look like "chrome.foo.bar".
+ virtual InstanceID* GetInstanceID(const std::string& app_id);
+
+ // Removes the InstanceID when it is not longer needed, i.e. the app is being
+ // uninstalled.
+ virtual void RemoveInstanceID(const std::string& app_id);
+
+ // Returns true if the InstanceID for the given application has been created.
+ // This is currently only used for testing purpose.
+ virtual bool ExistsInstanceID(const std::string& app_id) const;
+
+ private:
+ // Owned by GCMProfileServiceFactory, which is a dependency of
+ // InstanceIDProfileServiceFactory, which owns this.
+ raw_ptr<gcm::GCMDriver> gcm_driver_;
+
+ std::map<std::string, std::unique_ptr<InstanceID>> instance_id_map_;
+};
+
+} // namespace instance_id
+
+#endif // COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_DRIVER_H_
diff --git a/chromium/components/gcm_driver/instance_id/instance_id_driver_unittest.cc b/chromium/components/gcm_driver/instance_id/instance_id_driver_unittest.cc
new file mode 100644
index 00000000000..0170f499206
--- /dev/null
+++ b/chromium/components/gcm_driver/instance_id/instance_id_driver_unittest.cc
@@ -0,0 +1,366 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/instance_id/instance_id_driver.h"
+
+#include <stddef.h>
+
+#include <cmath>
+#include <memory>
+
+#include "base/bind.h"
+#include "base/run_loop.h"
+#include "base/strings/string_util.h"
+#include "base/test/task_environment.h"
+#include "components/gcm_driver/gcm_buildflags.h"
+#include "components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.h"
+#include "components/gcm_driver/instance_id/instance_id.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+#if BUILDFLAG(USE_GCM_FROM_PLATFORM)
+#include "components/gcm_driver/instance_id/instance_id_android.h"
+#include "components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h"
+#endif // BUILDFLAG(USE_GCM_FROM_PLATFORM)
+
+namespace instance_id {
+
+namespace {
+
+const char kTestAppID1[] = "TestApp1";
+const char kTestAppID2[] = "TestApp2";
+const char kAuthorizedEntity1[] = "Sender 1";
+const char kAuthorizedEntity2[] = "Sender 2";
+const char kScope1[] = "GCM1";
+const char kScope2[] = "FooBar";
+
+bool VerifyInstanceID(const std::string& str) {
+ // Checks the length.
+ if (str.length() != static_cast<size_t>(
+ std::ceil(InstanceID::kInstanceIDByteLength * 8 / 6.0)))
+ return false;
+
+ // Checks if it is URL-safe base64 encoded.
+ for (auto ch : str) {
+ if (!base::IsAsciiAlpha(ch) && !base::IsAsciiDigit(ch) &&
+ ch != '_' && ch != '-')
+ return false;
+ }
+ return true;
+}
+
+} // namespace
+
+class InstanceIDDriverTest : public testing::Test {
+ public:
+ InstanceIDDriverTest();
+
+ InstanceIDDriverTest(const InstanceIDDriverTest&) = delete;
+ InstanceIDDriverTest& operator=(const InstanceIDDriverTest&) = delete;
+
+ ~InstanceIDDriverTest() override;
+
+ // testing::Test:
+ void SetUp() override;
+ void TearDown() override;
+
+ void WaitForAsyncOperation();
+
+ // Recreates InstanceIDDriver to simulate restart.
+ void RecreateInstanceIDDriver();
+
+ // Sync wrappers for async version.
+ std::string GetID(InstanceID* instance_id);
+ base::Time GetCreationTime(InstanceID* instance_id);
+ InstanceID::Result DeleteID(InstanceID* instance_id);
+ std::string GetToken(InstanceID* instance_id,
+ const std::string& authorized_entity,
+ const std::string& scope);
+ InstanceID::Result DeleteToken(
+ InstanceID* instance_id,
+ const std::string& authorized_entity,
+ const std::string& scope);
+
+ InstanceIDDriver* driver() const { return driver_.get(); }
+
+ private:
+ void GetIDCompleted(const std::string& id);
+ void GetCreationTimeCompleted(const base::Time& creation_time);
+ void DeleteIDCompleted(InstanceID::Result result);
+ void GetTokenCompleted(const std::string& token, InstanceID::Result result);
+ void DeleteTokenCompleted(InstanceID::Result result);
+
+ base::test::SingleThreadTaskEnvironment task_environment_;
+ std::unique_ptr<FakeGCMDriverForInstanceID> gcm_driver_;
+ std::unique_ptr<InstanceIDDriver> driver_;
+
+#if BUILDFLAG(USE_GCM_FROM_PLATFORM)
+ InstanceIDAndroid::ScopedBlockOnAsyncTasksForTesting block_async_;
+ ScopedUseFakeInstanceIDAndroid use_fake_;
+#endif // BUILDFLAG(USE_GCM_FROM_PLATFORM)
+
+ std::string id_;
+ base::Time creation_time_;
+ std::string token_;
+ InstanceID::Result result_;
+
+ bool async_operation_completed_;
+ base::OnceClosure async_operation_completed_callback_;
+};
+
+InstanceIDDriverTest::InstanceIDDriverTest()
+ : task_environment_(
+ base::test::SingleThreadTaskEnvironment::MainThreadType::UI),
+ result_(InstanceID::UNKNOWN_ERROR),
+ async_operation_completed_(false) {}
+
+InstanceIDDriverTest::~InstanceIDDriverTest() {
+}
+
+void InstanceIDDriverTest::SetUp() {
+ gcm_driver_ = std::make_unique<FakeGCMDriverForInstanceID>();
+ RecreateInstanceIDDriver();
+}
+
+void InstanceIDDriverTest::TearDown() {
+ driver_.reset();
+ gcm_driver_.reset();
+ // |gcm_driver_| owns a GCMKeyStore that owns a ProtoDatabase whose
+ // destructor deletes the underlying LevelDB on the task runner.
+ base::RunLoop().RunUntilIdle();
+}
+
+void InstanceIDDriverTest::RecreateInstanceIDDriver() {
+ driver_ = std::make_unique<InstanceIDDriver>(gcm_driver_.get());
+}
+
+void InstanceIDDriverTest::WaitForAsyncOperation() {
+ // No need to wait if async operation is not needed.
+ if (async_operation_completed_)
+ return;
+ base::RunLoop run_loop;
+ async_operation_completed_callback_ = run_loop.QuitClosure();
+ run_loop.Run();
+}
+
+std::string InstanceIDDriverTest::GetID(InstanceID* instance_id) {
+ async_operation_completed_ = false;
+ id_.clear();
+ instance_id->GetID(base::BindOnce(&InstanceIDDriverTest::GetIDCompleted,
+ base::Unretained(this)));
+ WaitForAsyncOperation();
+ return id_;
+}
+
+base::Time InstanceIDDriverTest::GetCreationTime(InstanceID* instance_id) {
+ async_operation_completed_ = false;
+ creation_time_ = base::Time();
+ instance_id->GetCreationTime(base::BindOnce(
+ &InstanceIDDriverTest::GetCreationTimeCompleted, base::Unretained(this)));
+ WaitForAsyncOperation();
+ return creation_time_;
+}
+
+InstanceID::Result InstanceIDDriverTest::DeleteID(InstanceID* instance_id) {
+ async_operation_completed_ = false;
+ result_ = InstanceID::UNKNOWN_ERROR;
+ instance_id->DeleteID(base::BindOnce(&InstanceIDDriverTest::DeleteIDCompleted,
+ base::Unretained(this)));
+ WaitForAsyncOperation();
+ return result_;
+}
+
+std::string InstanceIDDriverTest::GetToken(InstanceID* instance_id,
+ const std::string& authorized_entity,
+ const std::string& scope) {
+ async_operation_completed_ = false;
+ token_.clear();
+ result_ = InstanceID::UNKNOWN_ERROR;
+ instance_id->GetToken(
+ authorized_entity, scope, /*time_to_live=*/base::TimeDelta(),
+ /*flags=*/{},
+ base::BindRepeating(&InstanceIDDriverTest::GetTokenCompleted,
+ base::Unretained(this)));
+ WaitForAsyncOperation();
+ return token_;
+}
+
+InstanceID::Result InstanceIDDriverTest::DeleteToken(
+ InstanceID* instance_id,
+ const std::string& authorized_entity,
+ const std::string& scope) {
+ async_operation_completed_ = false;
+ result_ = InstanceID::UNKNOWN_ERROR;
+ instance_id->DeleteToken(
+ authorized_entity, scope,
+ base::BindOnce(&InstanceIDDriverTest::DeleteTokenCompleted,
+ base::Unretained(this)));
+ WaitForAsyncOperation();
+ return result_;
+}
+
+void InstanceIDDriverTest::GetIDCompleted(const std::string& id) {
+ DCHECK(!async_operation_completed_);
+ async_operation_completed_ = true;
+ id_ = id;
+ if (async_operation_completed_callback_)
+ std::move(async_operation_completed_callback_).Run();
+}
+
+void InstanceIDDriverTest::GetCreationTimeCompleted(
+ const base::Time& creation_time) {
+ DCHECK(!async_operation_completed_);
+ async_operation_completed_ = true;
+ creation_time_ = creation_time;
+ if (async_operation_completed_callback_)
+ std::move(async_operation_completed_callback_).Run();
+}
+
+void InstanceIDDriverTest::DeleteIDCompleted(InstanceID::Result result) {
+ DCHECK(!async_operation_completed_);
+ async_operation_completed_ = true;
+ result_ = result;
+ if (async_operation_completed_callback_)
+ std::move(async_operation_completed_callback_).Run();
+}
+
+void InstanceIDDriverTest::GetTokenCompleted(
+ const std::string& token, InstanceID::Result result) {
+ DCHECK(!async_operation_completed_);
+ async_operation_completed_ = true;
+ token_ = token;
+ result_ = result;
+ if (async_operation_completed_callback_)
+ std::move(async_operation_completed_callback_).Run();
+}
+
+void InstanceIDDriverTest::DeleteTokenCompleted(InstanceID::Result result) {
+ DCHECK(!async_operation_completed_);
+ async_operation_completed_ = true;
+ result_ = result;
+ if (async_operation_completed_callback_)
+ std::move(async_operation_completed_callback_).Run();
+}
+
+TEST_F(InstanceIDDriverTest, GetAndRemoveInstanceID) {
+ EXPECT_FALSE(driver()->ExistsInstanceID(kTestAppID1));
+
+ InstanceID* instance_id = driver()->GetInstanceID(kTestAppID1);
+ EXPECT_TRUE(instance_id);
+ EXPECT_TRUE(driver()->ExistsInstanceID(kTestAppID1));
+
+ driver()->RemoveInstanceID(kTestAppID1);
+ EXPECT_FALSE(driver()->ExistsInstanceID(kTestAppID1));
+}
+
+TEST_F(InstanceIDDriverTest, NewID) {
+ // Creation time should not be set when the ID is not created.
+ InstanceID* instance_id1 = driver()->GetInstanceID(kTestAppID1);
+ EXPECT_TRUE(GetCreationTime(instance_id1).is_null());
+
+ // New ID is generated for the first time.
+ std::string id1 = GetID(instance_id1);
+ EXPECT_TRUE(VerifyInstanceID(id1));
+ base::Time creation_time = GetCreationTime(instance_id1);
+ EXPECT_FALSE(creation_time.is_null());
+
+ // Same ID is returned for the same app.
+ EXPECT_EQ(id1, GetID(instance_id1));
+ EXPECT_EQ(creation_time, GetCreationTime(instance_id1));
+
+ // New ID is generated for another app.
+ InstanceID* instance_id2 = driver()->GetInstanceID(kTestAppID2);
+ std::string id2 = GetID(instance_id2);
+ EXPECT_TRUE(VerifyInstanceID(id2));
+ EXPECT_NE(id1, id2);
+ EXPECT_FALSE(GetCreationTime(instance_id2).is_null());
+}
+
+TEST_F(InstanceIDDriverTest, PersistID) {
+ InstanceID* instance_id = driver()->GetInstanceID(kTestAppID1);
+
+ // Create the ID for the first time. The ID and creation time should be saved
+ // to the store.
+ std::string id = GetID(instance_id);
+ EXPECT_FALSE(id.empty());
+ base::Time creation_time = GetCreationTime(instance_id);
+ EXPECT_FALSE(creation_time.is_null());
+
+ // Simulate restart by recreating InstanceIDDriver. Same ID and creation time
+ // should be expected.
+ RecreateInstanceIDDriver();
+ instance_id = driver()->GetInstanceID(kTestAppID1);
+ EXPECT_EQ(creation_time, GetCreationTime(instance_id));
+ EXPECT_EQ(id, GetID(instance_id));
+
+ // Delete the ID. The ID and creation time should be removed from the store.
+ EXPECT_EQ(InstanceID::SUCCESS, DeleteID(instance_id));
+ EXPECT_TRUE(GetCreationTime(instance_id).is_null());
+
+ // Simulate restart by recreating InstanceIDDriver. Different ID should be
+ // expected.
+ // Note that we do not check for different creation time since the test might
+ // be run at a very fast server.
+ RecreateInstanceIDDriver();
+ instance_id = driver()->GetInstanceID(kTestAppID1);
+ EXPECT_NE(id, GetID(instance_id));
+}
+
+TEST_F(InstanceIDDriverTest, DeleteID) {
+ InstanceID* instance_id = driver()->GetInstanceID(kTestAppID1);
+ std::string id1 = GetID(instance_id);
+ EXPECT_FALSE(id1.empty());
+ EXPECT_FALSE(GetCreationTime(instance_id).is_null());
+
+ // New ID will be generated from GetID after calling DeleteID.
+ EXPECT_EQ(InstanceID::SUCCESS, DeleteID(instance_id));
+ EXPECT_TRUE(GetCreationTime(instance_id).is_null());
+
+ std::string id2 = GetID(instance_id);
+ EXPECT_FALSE(id2.empty());
+ EXPECT_NE(id1, id2);
+ EXPECT_FALSE(GetCreationTime(instance_id).is_null());
+}
+
+TEST_F(InstanceIDDriverTest, GetToken) {
+ InstanceID* instance_id = driver()->GetInstanceID(kTestAppID1);
+ std::string token1 = GetToken(instance_id, kAuthorizedEntity1, kScope1);
+ EXPECT_FALSE(token1.empty());
+
+ // Same token is returned for same authorized entity and scope.
+ EXPECT_EQ(token1, GetToken(instance_id, kAuthorizedEntity1, kScope1));
+
+ // Different token is returned for different authorized entity or scope.
+ std::string token2 = GetToken(instance_id, kAuthorizedEntity1, kScope2);
+ EXPECT_FALSE(token2.empty());
+ EXPECT_NE(token1, token2);
+
+ std::string token3 = GetToken(instance_id, kAuthorizedEntity2, kScope1);
+ EXPECT_FALSE(token3.empty());
+ EXPECT_NE(token1, token3);
+ EXPECT_NE(token2, token3);
+}
+
+TEST_F(InstanceIDDriverTest, DeleteToken) {
+ InstanceID* instance_id = driver()->GetInstanceID(kTestAppID1);
+
+ // Gets 2 tokens.
+ std::string token1 = GetToken(instance_id, kAuthorizedEntity1, kScope1);
+ EXPECT_FALSE(token1.empty());
+ std::string token2 = GetToken(instance_id, kAuthorizedEntity2, kScope1);
+ EXPECT_FALSE(token1.empty());
+ EXPECT_NE(token1, token2);
+
+ // Different token is returned for same authorized entity and scope after
+ // deletion.
+ EXPECT_EQ(InstanceID::SUCCESS,
+ DeleteToken(instance_id, kAuthorizedEntity1, kScope1));
+ std::string new_token1 = GetToken(instance_id, kAuthorizedEntity1, kScope2);
+ EXPECT_FALSE(new_token1.empty());
+ EXPECT_NE(token1, new_token1);
+
+ // The other token is not affected by the deletion.
+ EXPECT_EQ(token2, GetToken(instance_id, kAuthorizedEntity2, kScope1));
+}
+
+} // namespace instance_id
diff --git a/chromium/components/gcm_driver/instance_id/instance_id_impl.cc b/chromium/components/gcm_driver/instance_id/instance_id_impl.cc
new file mode 100644
index 00000000000..3f21ba69842
--- /dev/null
+++ b/chromium/components/gcm_driver/instance_id/instance_id_impl.cc
@@ -0,0 +1,275 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/instance_id/instance_id_impl.h"
+
+#include <stdint.h>
+
+#include <algorithm>
+#include <memory>
+
+#include "base/base64.h"
+#include "base/bind.h"
+#include "base/containers/cxx20_erase.h"
+#include "base/logging.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "components/gcm_driver/gcm_driver.h"
+#include "crypto/random.h"
+
+namespace instance_id {
+
+namespace {
+
+InstanceID::Result GCMClientResultToInstanceIDResult(
+ gcm::GCMClient::Result result) {
+ switch (result) {
+ case gcm::GCMClient::SUCCESS:
+ return InstanceID::SUCCESS;
+ case gcm::GCMClient::INVALID_PARAMETER:
+ return InstanceID::INVALID_PARAMETER;
+ case gcm::GCMClient::GCM_DISABLED:
+ return InstanceID::DISABLED;
+ case gcm::GCMClient::ASYNC_OPERATION_PENDING:
+ return InstanceID::ASYNC_OPERATION_PENDING;
+ case gcm::GCMClient::NETWORK_ERROR:
+ return InstanceID::NETWORK_ERROR;
+ case gcm::GCMClient::SERVER_ERROR:
+ return InstanceID::SERVER_ERROR;
+ case gcm::GCMClient::UNKNOWN_ERROR:
+ return InstanceID::UNKNOWN_ERROR;
+ case gcm::GCMClient::TTL_EXCEEDED:
+ NOTREACHED();
+ break;
+ }
+ return InstanceID::UNKNOWN_ERROR;
+}
+
+} // namespace
+
+// static
+std::unique_ptr<InstanceID> InstanceID::CreateInternal(
+ const std::string& app_id,
+ gcm::GCMDriver* gcm_driver) {
+ return std::make_unique<InstanceIDImpl>(app_id, gcm_driver);
+}
+
+InstanceIDImpl::InstanceIDImpl(const std::string& app_id,
+ gcm::GCMDriver* gcm_driver)
+ : InstanceID(app_id, gcm_driver) {
+ Handler()->GetInstanceIDData(
+ app_id, base::BindOnce(&InstanceIDImpl::GetInstanceIDDataCompleted,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+InstanceIDImpl::~InstanceIDImpl() {
+}
+
+void InstanceIDImpl::GetID(GetIDCallback callback) {
+ RunWhenReady(base::BindOnce(&InstanceIDImpl::DoGetID,
+ weak_ptr_factory_.GetWeakPtr(),
+ std::move(callback)));
+}
+
+void InstanceIDImpl::DoGetID(GetIDCallback callback) {
+ EnsureIDGenerated();
+ std::move(callback).Run(id_);
+}
+
+void InstanceIDImpl::GetCreationTime(GetCreationTimeCallback callback) {
+ RunWhenReady(base::BindOnce(&InstanceIDImpl::DoGetCreationTime,
+ weak_ptr_factory_.GetWeakPtr(),
+ std::move(callback)));
+}
+
+void InstanceIDImpl::DoGetCreationTime(GetCreationTimeCallback callback) {
+ std::move(callback).Run(creation_time_);
+}
+
+void InstanceIDImpl::GetToken(const std::string& authorized_entity,
+ const std::string& scope,
+ base::TimeDelta time_to_live,
+ std::set<Flags> flags,
+ GetTokenCallback callback) {
+ DCHECK(!authorized_entity.empty());
+ DCHECK(!scope.empty());
+
+ RunWhenReady(base::BindOnce(&InstanceIDImpl::DoGetToken,
+ weak_ptr_factory_.GetWeakPtr(), authorized_entity,
+ scope, time_to_live, std::move(callback)));
+}
+
+void InstanceIDImpl::DoGetToken(
+ const std::string& authorized_entity,
+ const std::string& scope,
+ base::TimeDelta time_to_live,
+ GetTokenCallback callback) {
+ EnsureIDGenerated();
+
+ Handler()->GetToken(
+ app_id(), authorized_entity, scope, time_to_live,
+ base::BindOnce(&InstanceIDImpl::OnGetTokenCompleted,
+ weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+}
+
+void InstanceIDImpl::ValidateToken(const std::string& authorized_entity,
+ const std::string& scope,
+ const std::string& token,
+ ValidateTokenCallback callback) {
+ DCHECK(!authorized_entity.empty());
+ DCHECK(!scope.empty());
+ DCHECK(!token.empty());
+
+ RunWhenReady(base::BindOnce(&InstanceIDImpl::DoValidateToken,
+ weak_ptr_factory_.GetWeakPtr(), authorized_entity,
+ scope, token, std::move(callback)));
+}
+
+void InstanceIDImpl::DoValidateToken(const std::string& authorized_entity,
+ const std::string& scope,
+ const std::string& token,
+ ValidateTokenCallback callback) {
+ if (id_.empty()) {
+ std::move(callback).Run(false /* is_valid */);
+ return;
+ }
+
+ Handler()->ValidateToken(app_id(), authorized_entity, scope, token,
+ std::move(callback));
+}
+
+void InstanceIDImpl::DeleteTokenImpl(const std::string& authorized_entity,
+ const std::string& scope,
+ DeleteTokenCallback callback) {
+ DCHECK(!authorized_entity.empty());
+ DCHECK(!scope.empty());
+
+ RunWhenReady(base::BindOnce(&InstanceIDImpl::DoDeleteToken,
+ weak_ptr_factory_.GetWeakPtr(), authorized_entity,
+ scope, std::move(callback)));
+}
+
+void InstanceIDImpl::DoDeleteToken(const std::string& authorized_entity,
+ const std::string& scope,
+ DeleteTokenCallback callback) {
+ // Nothing to delete if the ID has not been generated.
+ if (id_.empty()) {
+ std::move(callback).Run(InstanceID::INVALID_PARAMETER);
+ return;
+ }
+
+ Handler()->DeleteToken(
+ app_id(), authorized_entity, scope,
+ base::BindOnce(&InstanceIDImpl::OnDeleteTokenCompleted,
+ weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+}
+
+void InstanceIDImpl::DeleteIDImpl(DeleteIDCallback callback) {
+ RunWhenReady(base::BindOnce(&InstanceIDImpl::DoDeleteID,
+ weak_ptr_factory_.GetWeakPtr(),
+ std::move(callback)));
+}
+
+void InstanceIDImpl::DoDeleteID(DeleteIDCallback callback) {
+ // Nothing to do if ID has not been generated.
+ if (id_.empty()) {
+ std::move(callback).Run(InstanceID::SUCCESS);
+ return;
+ }
+
+ Handler()->DeleteAllTokensForApp(
+ app_id(),
+ base::BindOnce(&InstanceIDImpl::OnDeleteIDCompleted,
+ weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+
+ Handler()->RemoveInstanceIDData(app_id());
+
+ id_.clear();
+ creation_time_ = base::Time();
+}
+
+void InstanceIDImpl::OnGetTokenCompleted(GetTokenCallback callback,
+ const std::string& token,
+ gcm::GCMClient::Result result) {
+ std::move(callback).Run(token, GCMClientResultToInstanceIDResult(result));
+}
+
+void InstanceIDImpl::OnDeleteTokenCompleted(DeleteTokenCallback callback,
+ gcm::GCMClient::Result result) {
+ std::move(callback).Run(GCMClientResultToInstanceIDResult(result));
+}
+
+void InstanceIDImpl::OnDeleteIDCompleted(DeleteIDCallback callback,
+ gcm::GCMClient::Result result) {
+ std::move(callback).Run(GCMClientResultToInstanceIDResult(result));
+}
+
+void InstanceIDImpl::GetInstanceIDDataCompleted(
+ const std::string& instance_id,
+ const std::string& extra_data) {
+ id_ = instance_id;
+
+ if (extra_data.empty()) {
+ creation_time_ = base::Time();
+ } else {
+ int64_t time_internal = 0LL;
+ if (!base::StringToInt64(extra_data, &time_internal)) {
+ DVLOG(1) << "Failed to parse the time data: " + extra_data;
+ return;
+ }
+ creation_time_ = base::Time::FromInternalValue(time_internal);
+ }
+
+ delayed_task_controller_.SetReady();
+}
+
+void InstanceIDImpl::EnsureIDGenerated() {
+ if (!id_.empty())
+ return;
+
+ // Now produce the ID in the following steps:
+
+ // 1) Generates the random number in 8 bytes which is required by the server.
+ // We don't want to be strictly cryptographically secure. The server might
+ // reject the ID if there is a conflict or problem.
+ uint8_t bytes[kInstanceIDByteLength];
+ crypto::RandBytes(bytes, sizeof(bytes));
+
+ // 2) Transforms the first 4 bits to 0x7. Note that this is required by the
+ // server.
+ bytes[0] &= 0x0f;
+ bytes[0] |= 0x70;
+
+ // 3) Encode the value in Android-compatible base64 scheme:
+ // * URL safe: '/' replaced by '_' and '+' replaced by '-'.
+ // * No padding: any trailing '=' will be removed.
+ base::Base64Encode(
+ base::StringPiece(reinterpret_cast<const char*>(bytes), sizeof(bytes)),
+ &id_);
+ std::replace(id_.begin(), id_.end(), '+', '-');
+ std::replace(id_.begin(), id_.end(), '/', '_');
+ base::Erase(id_, '=');
+
+ creation_time_ = base::Time::Now();
+
+ // Save to the persistent store.
+ Handler()->AddInstanceIDData(
+ app_id(), id_, base::NumberToString(creation_time_.ToInternalValue()));
+}
+
+gcm::InstanceIDHandler* InstanceIDImpl::Handler() {
+ gcm::InstanceIDHandler* handler =
+ gcm_driver()->GetInstanceIDHandlerInternal();
+ DCHECK(handler);
+ return handler;
+}
+
+void InstanceIDImpl::RunWhenReady(base::OnceClosure task) {
+ if (!delayed_task_controller_.CanRunTaskWithoutDelay())
+ delayed_task_controller_.AddTask(std::move(task));
+ else
+ base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, std::move(task));
+}
+
+} // namespace instance_id
diff --git a/chromium/components/gcm_driver/instance_id/instance_id_impl.h b/chromium/components/gcm_driver/instance_id/instance_id_impl.h
new file mode 100644
index 00000000000..e39fd1e242c
--- /dev/null
+++ b/chromium/components/gcm_driver/instance_id/instance_id_impl.h
@@ -0,0 +1,98 @@
+// Copyright 2015 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 COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_IMPL_H_
+#define COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_IMPL_H_
+
+#include <memory>
+#include <string>
+
+#include "base/callback.h"
+#include "base/memory/weak_ptr.h"
+#include "base/time/time.h"
+#include "components/gcm_driver/gcm_client.h"
+#include "components/gcm_driver/gcm_delayed_task_controller.h"
+#include "components/gcm_driver/instance_id/instance_id.h"
+
+namespace gcm {
+class GCMDriver;
+class InstanceIDHandler;
+} // namespace gcm
+
+namespace instance_id {
+
+// InstanceID implementation for desktop and iOS.
+class InstanceIDImpl : public InstanceID {
+ public:
+ InstanceIDImpl(const std::string& app_id, gcm::GCMDriver* gcm_driver);
+
+ InstanceIDImpl(const InstanceIDImpl&) = delete;
+ InstanceIDImpl& operator=(const InstanceIDImpl&) = delete;
+
+ ~InstanceIDImpl() override;
+
+ // InstanceID:
+ void GetID(GetIDCallback callback) override;
+ void GetCreationTime(GetCreationTimeCallback callback) override;
+ void GetToken(const std::string& authorized_entity,
+ const std::string& scope,
+ base::TimeDelta time_to_live,
+ std::set<Flags> flags,
+ GetTokenCallback callback) override;
+ void ValidateToken(const std::string& authorized_entity,
+ const std::string& scope,
+ const std::string& token,
+ ValidateTokenCallback callback) override;
+ void DeleteTokenImpl(const std::string& authorized_entity,
+ const std::string& scope,
+ DeleteTokenCallback callback) override;
+ void DeleteIDImpl(DeleteIDCallback callback) override;
+
+ private:
+ void EnsureIDGenerated();
+
+ void OnGetTokenCompleted(GetTokenCallback callback,
+ const std::string& token,
+ gcm::GCMClient::Result result);
+ void OnDeleteTokenCompleted(DeleteTokenCallback callback,
+ gcm::GCMClient::Result result);
+ void OnDeleteIDCompleted(DeleteIDCallback callback,
+ gcm::GCMClient::Result result);
+ void GetInstanceIDDataCompleted(const std::string& instance_id,
+ const std::string& extra_data);
+
+ void DoGetID(GetIDCallback callback);
+ void DoGetCreationTime(GetCreationTimeCallback callback);
+ void DoGetToken(const std::string& authorized_entity,
+ const std::string& scope,
+ base::TimeDelta time_to_live,
+ GetTokenCallback callback);
+ void DoValidateToken(const std::string& authorized_entity,
+ const std::string& scope,
+ const std::string& token,
+ ValidateTokenCallback callback);
+ void DoDeleteToken(const std::string& authorized_entity,
+ const std::string& scope,
+ DeleteTokenCallback callback);
+ void DoDeleteID(DeleteIDCallback callback);
+
+ gcm::InstanceIDHandler* Handler();
+
+ // Asynchronously runs task once delayed_task_controller_ is ready.
+ void RunWhenReady(base::OnceClosure task);
+
+ gcm::GCMDelayedTaskController delayed_task_controller_;
+
+ // The generated Instance ID.
+ std::string id_;
+
+ // The time when the Instance ID has been generated.
+ base::Time creation_time_;
+
+ base::WeakPtrFactory<InstanceIDImpl> weak_ptr_factory_{this};
+};
+
+} // namespace instance_id
+
+#endif // COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_IMPL_H_
diff --git a/chromium/components/gcm_driver/instance_id/instance_id_profile_service.cc b/chromium/components/gcm_driver/instance_id/instance_id_profile_service.cc
new file mode 100644
index 00000000000..ecbafd3d4ad
--- /dev/null
+++ b/chromium/components/gcm_driver/instance_id/instance_id_profile_service.cc
@@ -0,0 +1,23 @@
+// Copyright (c) 2015 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.
+
+#include "components/gcm_driver/instance_id/instance_id_profile_service.h"
+
+#include "base/check.h"
+#include "components/gcm_driver/gcm_driver.h"
+#include "components/gcm_driver/gcm_profile_service.h"
+#include "components/gcm_driver/instance_id/instance_id_driver.h"
+
+namespace instance_id {
+
+InstanceIDProfileService::InstanceIDProfileService(gcm::GCMDriver* driver,
+ bool is_off_the_record) {
+ DCHECK(!is_off_the_record);
+
+ driver_ = std::make_unique<InstanceIDDriver>(driver);
+}
+
+InstanceIDProfileService::~InstanceIDProfileService() {}
+
+} // namespace instance_id
diff --git a/chromium/components/gcm_driver/instance_id/instance_id_profile_service.h b/chromium/components/gcm_driver/instance_id/instance_id_profile_service.h
new file mode 100644
index 00000000000..8757c086250
--- /dev/null
+++ b/chromium/components/gcm_driver/instance_id/instance_id_profile_service.h
@@ -0,0 +1,38 @@
+// Copyright (c) 2015 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 COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_PROFILE_SERVICE_H_
+#define COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_PROFILE_SERVICE_H_
+
+#include <memory>
+
+#include "components/keyed_service/core/keyed_service.h"
+
+namespace gcm {
+class GCMDriver;
+}
+
+namespace instance_id {
+
+class InstanceIDDriver;
+
+// Providing Instance ID support, via InstanceIDDriver, to a profile.
+class InstanceIDProfileService : public KeyedService {
+ public:
+ InstanceIDProfileService(gcm::GCMDriver* driver, bool is_off_the_record);
+
+ InstanceIDProfileService(const InstanceIDProfileService&) = delete;
+ InstanceIDProfileService& operator=(const InstanceIDProfileService&) = delete;
+
+ ~InstanceIDProfileService() override;
+
+ InstanceIDDriver* driver() const { return driver_.get(); }
+
+ private:
+ std::unique_ptr<InstanceIDDriver> driver_;
+};
+
+} // namespace instance_id
+
+#endif // COMPONENTS_GCM_DRIVER_INSTANCE_ID_INSTANCE_ID_PROFILE_SERVICE_H_
diff --git a/chromium/components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.cc b/chromium/components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.cc
new file mode 100644
index 00000000000..585e83d6b6d
--- /dev/null
+++ b/chromium/components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.cc
@@ -0,0 +1,25 @@
+// Copyright 2016 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.
+
+#include "components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h"
+
+#include "base/android/jni_android.h"
+#include "components/gcm_driver/instance_id/android/test_support_jni_headers/FakeInstanceIDWithSubtype_jni.h"
+
+using base::android::AttachCurrentThread;
+
+namespace instance_id {
+
+ScopedUseFakeInstanceIDAndroid::ScopedUseFakeInstanceIDAndroid() {
+ JNIEnv* env = AttachCurrentThread();
+ previous_value_ =
+ Java_FakeInstanceIDWithSubtype_clearDataAndSetEnabled(env, true);
+}
+
+ScopedUseFakeInstanceIDAndroid::~ScopedUseFakeInstanceIDAndroid() {
+ JNIEnv* env = AttachCurrentThread();
+ Java_FakeInstanceIDWithSubtype_clearDataAndSetEnabled(env, previous_value_);
+}
+
+} // namespace instance_id
diff --git a/chromium/components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h b/chromium/components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h
new file mode 100644
index 00000000000..75dc5c175eb
--- /dev/null
+++ b/chromium/components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h
@@ -0,0 +1,31 @@
+// Copyright 2016 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 COMPONENTS_GCM_DRIVER_INSTANCE_ID_SCOPED_USE_FAKE_INSTANCE_ID_ANDROID_H_
+#define COMPONENTS_GCM_DRIVER_INSTANCE_ID_SCOPED_USE_FAKE_INSTANCE_ID_ANDROID_H_
+
+#include <jni.h>
+
+namespace instance_id {
+
+// Tests depending on InstanceID must use this, to avoid hitting the
+// network/disk. Also clears cached InstanceIDs when constructed/destructed.
+class ScopedUseFakeInstanceIDAndroid {
+ public:
+ ScopedUseFakeInstanceIDAndroid();
+
+ ScopedUseFakeInstanceIDAndroid(const ScopedUseFakeInstanceIDAndroid&) =
+ delete;
+ ScopedUseFakeInstanceIDAndroid& operator=(
+ const ScopedUseFakeInstanceIDAndroid&) = delete;
+
+ ~ScopedUseFakeInstanceIDAndroid();
+
+ private:
+ bool previous_value_;
+};
+
+} // namespace instance_id
+
+#endif // COMPONENTS_GCM_DRIVER_INSTANCE_ID_SCOPED_USE_FAKE_INSTANCE_ID_ANDROID_H_
diff --git a/chromium/components/gcm_driver/registration_info.cc b/chromium/components/gcm_driver/registration_info.cc
new file mode 100644
index 00000000000..13689bbbe8c
--- /dev/null
+++ b/chromium/components/gcm_driver/registration_info.cc
@@ -0,0 +1,286 @@
+// Copyright 2015 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.
+
+#include "components/gcm_driver/registration_info.h"
+
+#include <stddef.h>
+
+#include "base/format_macros.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_split.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+
+namespace gcm {
+
+namespace {
+constexpr char kInstanceIDSerializationPrefix[] = "iid-";
+constexpr char kSerializedValidationTimeSeparator = '#';
+constexpr char kSerializedKeySeparator = ',';
+constexpr int kInstanceIDSerializationPrefixLength =
+ sizeof(kInstanceIDSerializationPrefix) / sizeof(char) - 1;
+} // namespace
+
+// static
+scoped_refptr<RegistrationInfo> RegistrationInfo::BuildFromString(
+ const std::string& serialized_key,
+ const std::string& serialized_value,
+ std::string* registration_id) {
+ scoped_refptr<RegistrationInfo> registration;
+
+ if (base::StartsWith(serialized_key, kInstanceIDSerializationPrefix,
+ base::CompareCase::SENSITIVE)) {
+ registration = base::MakeRefCounted<InstanceIDTokenInfo>();
+ } else {
+ registration = base::MakeRefCounted<GCMRegistrationInfo>();
+ }
+
+ if (!registration->Deserialize(serialized_key, serialized_value,
+ registration_id)) {
+ registration.reset();
+ }
+ return registration;
+}
+
+RegistrationInfo::RegistrationInfo() = default;
+
+RegistrationInfo::~RegistrationInfo() = default;
+
+// static
+const GCMRegistrationInfo* GCMRegistrationInfo::FromRegistrationInfo(
+ const RegistrationInfo* registration_info) {
+ if (!registration_info || registration_info->GetType() != GCM_REGISTRATION)
+ return nullptr;
+ return static_cast<const GCMRegistrationInfo*>(registration_info);
+}
+
+// static
+GCMRegistrationInfo* GCMRegistrationInfo::FromRegistrationInfo(
+ RegistrationInfo* registration_info) {
+ if (!registration_info || registration_info->GetType() != GCM_REGISTRATION)
+ return nullptr;
+ return static_cast<GCMRegistrationInfo*>(registration_info);
+}
+
+GCMRegistrationInfo::GCMRegistrationInfo() = default;
+
+GCMRegistrationInfo::~GCMRegistrationInfo() = default;
+
+RegistrationInfo::RegistrationType GCMRegistrationInfo::GetType() const {
+ return GCM_REGISTRATION;
+}
+
+std::string GCMRegistrationInfo::GetSerializedKey() const {
+ // Multiple registrations are not supported for legacy GCM. So the key is
+ // purely based on the application id.
+ return app_id;
+}
+
+std::string GCMRegistrationInfo::GetSerializedValue(
+ const std::string& registration_id) const {
+ if (sender_ids.empty() || registration_id.empty())
+ return std::string();
+
+ // Serialize as:
+ // sender1,sender2,...=reg_id#time_of_last_validation
+ std::string value;
+ for (auto iter = sender_ids.begin(); iter != sender_ids.end(); ++iter) {
+ DCHECK(!iter->empty() &&
+ iter->find(',') == std::string::npos &&
+ iter->find('=') == std::string::npos);
+ if (!value.empty())
+ value += ",";
+ value += *iter;
+ }
+
+ return base::StringPrintf("%s=%s%c%" PRId64, value.c_str(),
+ registration_id.c_str(),
+ kSerializedValidationTimeSeparator,
+ last_validated.since_origin().InMicroseconds());
+}
+
+bool GCMRegistrationInfo::Deserialize(const std::string& serialized_key,
+ const std::string& serialized_value,
+ std::string* registration_id) {
+ if (serialized_key.empty() || serialized_value.empty())
+ return false;
+
+ // Application ID is same as the serialized key.
+ app_id = serialized_key;
+
+ // Sender IDs and registration ID are constructed from the serialized value.
+ size_t pos_equals = serialized_value.find('=');
+ if (pos_equals == std::string::npos)
+ return false;
+ // Note that it's valid for pos_hash to be std::string::npos.
+ size_t pos_hash = serialized_value.find(kSerializedValidationTimeSeparator);
+ bool has_timestamp = pos_hash != std::string::npos;
+
+ std::string senders = serialized_value.substr(0, pos_equals);
+ std::string registration_id_str, last_validated_str;
+ if (has_timestamp) {
+ registration_id_str =
+ serialized_value.substr(pos_equals + 1, pos_hash - pos_equals - 1);
+ last_validated_str = serialized_value.substr(pos_hash + 1);
+ } else {
+ registration_id_str = serialized_value.substr(pos_equals + 1);
+ }
+
+ sender_ids = base::SplitString(
+ senders, ",", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
+ if (sender_ids.empty() || registration_id_str.empty()) {
+ sender_ids.clear();
+ registration_id_str.clear();
+ return false;
+ }
+
+ if (registration_id)
+ *registration_id = registration_id_str;
+ int64_t last_validated_ms = 0;
+ if (base::StringToInt64(last_validated_str, &last_validated_ms)) {
+ // It's okay for |last_validated| to be the default base::Time() value
+ // when there is no serialized timestamp value available.
+ last_validated = base::Time() + base::Microseconds(last_validated_ms);
+ }
+
+ return true;
+}
+
+// static
+const InstanceIDTokenInfo* InstanceIDTokenInfo::FromRegistrationInfo(
+ const RegistrationInfo* registration_info) {
+ if (!registration_info || registration_info->GetType() != INSTANCE_ID_TOKEN)
+ return nullptr;
+ return static_cast<const InstanceIDTokenInfo*>(registration_info);
+}
+
+// static
+InstanceIDTokenInfo* InstanceIDTokenInfo::FromRegistrationInfo(
+ RegistrationInfo* registration_info) {
+ if (!registration_info || registration_info->GetType() != INSTANCE_ID_TOKEN)
+ return nullptr;
+ return static_cast<InstanceIDTokenInfo*>(registration_info);
+}
+
+InstanceIDTokenInfo::InstanceIDTokenInfo() = default;
+
+InstanceIDTokenInfo::~InstanceIDTokenInfo() = default;
+
+RegistrationInfo::RegistrationType InstanceIDTokenInfo::GetType() const {
+ return INSTANCE_ID_TOKEN;
+}
+
+std::string InstanceIDTokenInfo::GetSerializedKey() const {
+ DCHECK(app_id.find(',') == std::string::npos &&
+ authorized_entity.find(',') == std::string::npos &&
+ scope.find(',') == std::string::npos);
+
+ // Multiple registrations are supported for Instance ID. So the key is based
+ // on the combination of (app_id, authorized_entity, scope).
+
+ // Adds a prefix to differentiate easily with GCM registration key.
+ return base::StringPrintf("%s%s%c%s%c%s", kInstanceIDSerializationPrefix,
+ app_id.c_str(), kSerializedKeySeparator,
+ authorized_entity.c_str(), kSerializedKeySeparator,
+ scope.c_str());
+}
+
+std::string InstanceIDTokenInfo::GetSerializedValue(
+ const std::string& registration_id) const {
+ int64_t last_validated_ms = last_validated.since_origin().InMicroseconds();
+ return registration_id + kSerializedValidationTimeSeparator +
+ base::NumberToString(last_validated_ms);
+}
+
+bool InstanceIDTokenInfo::Deserialize(const std::string& serialized_key,
+ const std::string& serialized_value,
+ std::string* registration_id) {
+ if (serialized_key.empty() || serialized_value.empty())
+ return false;
+
+ if (!base::StartsWith(serialized_key, kInstanceIDSerializationPrefix,
+ base::CompareCase::SENSITIVE))
+ return false;
+
+ std::vector<std::string> fields = base::SplitString(
+ serialized_key.substr(kInstanceIDSerializationPrefixLength), ",",
+ base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
+ if (fields.size() != 3 || fields[0].empty() ||
+ fields[1].empty() || fields[2].empty()) {
+ return false;
+ }
+ app_id = fields[0];
+ authorized_entity = fields[1];
+ scope = fields[2];
+
+ // Get Registration ID and last_validated from serialized value
+ size_t pos_hash = serialized_value.find(kSerializedValidationTimeSeparator);
+ bool has_timestamp = (pos_hash != std::string::npos);
+
+ std::string registration_id_str, last_validated_str;
+ if (has_timestamp) {
+ registration_id_str = serialized_value.substr(0, pos_hash);
+ last_validated_str = serialized_value.substr(pos_hash + 1);
+ } else {
+ registration_id_str = serialized_value;
+ }
+
+ if (registration_id)
+ *registration_id = registration_id_str;
+
+ int64_t last_validated_ms = 0;
+ if (base::StringToInt64(last_validated_str, &last_validated_ms)) {
+ // It's okay for last_validated to be the default base::Time() value
+ // when there is no serialized timestamp available.
+ last_validated += base::Microseconds(last_validated_ms);
+ }
+
+ return true;
+}
+
+bool RegistrationInfoComparer::operator()(
+ const scoped_refptr<RegistrationInfo>& a,
+ const scoped_refptr<RegistrationInfo>& b) const {
+ DCHECK(a.get() && b.get());
+
+ // For GCMRegistrationInfo, the comparison is based on app_id only.
+ // For InstanceIDTokenInfo, the comparison is based on
+ // <app_id, authorized_entity, scope>.
+ if (a->app_id < b->app_id)
+ return true;
+ if (a->app_id > b->app_id)
+ return false;
+
+ InstanceIDTokenInfo* iid_a =
+ InstanceIDTokenInfo::FromRegistrationInfo(a.get());
+ InstanceIDTokenInfo* iid_b =
+ InstanceIDTokenInfo::FromRegistrationInfo(b.get());
+
+ // !iid_a && !iid_b => false.
+ // !iid_a && iid_b => true.
+ // This makes GCM record is sorted before InstanceID record.
+ if (!iid_a)
+ return iid_b != nullptr;
+
+ // iid_a && !iid_b => false.
+ if (!iid_b)
+ return false;
+
+ // Otherwise, compare with authorized_entity and scope.
+ if (iid_a->authorized_entity < iid_b->authorized_entity)
+ return true;
+ if (iid_a->authorized_entity > iid_b->authorized_entity)
+ return false;
+ return iid_a->scope < iid_b->scope;
+}
+
+bool ExistsGCMRegistrationInMap(const RegistrationInfoMap& map,
+ const std::string& app_id) {
+ scoped_refptr<RegistrationInfo> gcm_registration =
+ base::MakeRefCounted<GCMRegistrationInfo>();
+ gcm_registration->app_id = app_id;
+ return map.find(gcm_registration) != map.end();
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/registration_info.h b/chromium/components/gcm_driver/registration_info.h
new file mode 100644
index 00000000000..b5364c2cdf4
--- /dev/null
+++ b/chromium/components/gcm_driver/registration_info.h
@@ -0,0 +1,137 @@
+// Copyright 2015 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 COMPONENTS_GCM_DRIVER_REGISTRATION_INFO_H_
+#define COMPONENTS_GCM_DRIVER_REGISTRATION_INFO_H_
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/memory/ref_counted.h"
+#include "base/time/time.h"
+
+namespace gcm {
+
+// Encapsulates the information needed to register with the server.
+struct RegistrationInfo : public base::RefCounted<RegistrationInfo> {
+ enum RegistrationType {
+ GCM_REGISTRATION,
+ INSTANCE_ID_TOKEN
+ };
+
+ // Returns the appropriate RegistrationInfo instance based on the serialized
+ // key and value.
+ // |registration_id| can be NULL if no interest to it.
+ static scoped_refptr<RegistrationInfo> BuildFromString(
+ const std::string& serialized_key,
+ const std::string& serialized_value,
+ std::string* registration_id);
+
+ RegistrationInfo();
+
+ // Returns the type of the registration info.
+ virtual RegistrationType GetType() const = 0;
+
+ // For persisting to the store. Depending on the type, part of the
+ // registration info is written as key. The remaining of the registration
+ // info plus the registration ID are written as value.
+ virtual std::string GetSerializedKey() const = 0;
+ virtual std::string GetSerializedValue(
+ const std::string& registration_id) const = 0;
+ // |registration_id| can be NULL if it is of no interest to the caller.
+ virtual bool Deserialize(const std::string& serialized_key,
+ const std::string& serialized_value,
+ std::string* registration_id) = 0;
+
+ // Every registration is associated with an application.
+ std::string app_id;
+ base::Time last_validated;
+
+ protected:
+ friend class base::RefCounted<RegistrationInfo>;
+ virtual ~RegistrationInfo();
+};
+
+// For GCM registration.
+struct GCMRegistrationInfo final : public RegistrationInfo {
+ GCMRegistrationInfo();
+
+ // Converts from the base type;
+ static const GCMRegistrationInfo* FromRegistrationInfo(
+ const RegistrationInfo* registration_info);
+ static GCMRegistrationInfo* FromRegistrationInfo(
+ RegistrationInfo* registration_info);
+
+ // RegistrationInfo overrides:
+ RegistrationType GetType() const override;
+ std::string GetSerializedKey() const override;
+ std::string GetSerializedValue(
+ const std::string& registration_id) const override;
+ bool Deserialize(const std::string& serialized_key,
+ const std::string& serialized_value,
+ std::string* registration_id) override;
+
+ // List of IDs of the servers that are allowed to send the messages to the
+ // application. These IDs are assigned by the Google API Console.
+ std::vector<std::string> sender_ids;
+
+ private:
+ ~GCMRegistrationInfo() override;
+};
+
+// For InstanceID token retrieval.
+struct InstanceIDTokenInfo final : public RegistrationInfo {
+ InstanceIDTokenInfo();
+
+ // Converts from the base type;
+ static const InstanceIDTokenInfo* FromRegistrationInfo(
+ const RegistrationInfo* registration_info);
+ static InstanceIDTokenInfo* FromRegistrationInfo(
+ RegistrationInfo* registration_info);
+
+ // RegistrationInfo overrides:
+ RegistrationType GetType() const override;
+ std::string GetSerializedKey() const override;
+ std::string GetSerializedValue(
+ const std::string& registration_id) const override;
+ bool Deserialize(const std::string& serialized_key,
+ const std::string& serialized_value,
+ std::string* registration_id) override;
+
+ // Entity that is authorized to access resources associated with the Instance
+ // ID. It can be another Instance ID or a project ID assigned by the Google
+ // API Console.
+ std::string authorized_entity;
+
+ // Authorized actions that the authorized entity can take.
+ // E.g. for sending GCM messages, 'GCM' scope should be used.
+ std::string scope;
+
+ // Specifies TTL of retrievable token, zero value means unlimited TTL.
+ // Not serialized/deserialized.
+ base::TimeDelta time_to_live;
+
+ private:
+ ~InstanceIDTokenInfo() override;
+};
+
+struct RegistrationInfoComparer {
+ bool operator()(const scoped_refptr<RegistrationInfo>& a,
+ const scoped_refptr<RegistrationInfo>& b) const;
+};
+
+// Collection of registration info.
+// Map from RegistrationInfo instance to registration ID.
+using RegistrationInfoMap = std::
+ map<scoped_refptr<RegistrationInfo>, std::string, RegistrationInfoComparer>;
+
+// Returns true if a GCM registration for |app_id| exists in |map|.
+bool ExistsGCMRegistrationInMap(const RegistrationInfoMap& map,
+ const std::string& app_id);
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_REGISTRATION_INFO_H_
diff --git a/chromium/components/gcm_driver/resources/OWNERS b/chromium/components/gcm_driver/resources/OWNERS
new file mode 100644
index 00000000000..c05db2e16e1
--- /dev/null
+++ b/chromium/components/gcm_driver/resources/OWNERS
@@ -0,0 +1,2 @@
+# For trivial or mechanical horizontal JS/CSS/HTML changes.
+file://ui/webui/PLATFORM_OWNERS
diff --git a/chromium/components/gcm_driver/resources/gcm_internals.css b/chromium/components/gcm_driver/resources/gcm_internals.css
new file mode 100644
index 00000000000..82ebe76a375
--- /dev/null
+++ b/chromium/components/gcm_driver/resources/gcm_internals.css
@@ -0,0 +1,47 @@
+/* Copyright 2014 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. */
+
+h1 {
+ color: rgb(74, 142, 230);
+ margin: 0;
+ padding: 0;
+}
+
+td {
+ padding: 4px;
+}
+
+tr:nth-child(odd) {
+ background-color: rgb(245, 245, 200);
+}
+
+th {
+ background-color: rgb(160, 160, 125);
+ color: rgb(255, 255, 255);
+ font-weight: bold;
+}
+
+.flexbar {
+ display: flex;
+ flex-direction: row;
+ margin: 5px 0px;
+}
+
+.flexbar button {
+ padding: 0px 4px;
+}
+
+#device-info tr > :first-child {
+ font-weight: bold;
+ padding-right: 10px;
+ text-align: end;
+}
+
+.log-table {
+ padding: 4px;
+}
+
+#android-secret-container.invisible {
+ display: none;
+}
diff --git a/chromium/components/gcm_driver/resources/gcm_internals.html b/chromium/components/gcm_driver/resources/gcm_internals.html
new file mode 100644
index 00000000000..70c3856c5e7
--- /dev/null
+++ b/chromium/components/gcm_driver/resources/gcm_internals.html
@@ -0,0 +1,210 @@
+<!doctype html>
+<html dir="ltr" lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>GCM Internals</title>
+ <if expr="is_android">
+ <meta name="viewport" content="width=device-width">
+ </if>
+ <link rel="stylesheet" href="chrome://resources/css/text_defaults.css">
+ <link rel="stylesheet" href="gcm_internals.css">
+ <script type="module" src="gcm_internals.js"></script>
+</head>
+<body>
+<h1>GCM Internals</h1>
+<div class="flexbar">
+ <button id="refresh">Refresh</button>
+ <button id="recording">Start Recording</button>
+ <button id="clear-logs">Clear All Logs</button>
+</div>
+
+<h2>Device Info</h2>
+<table id="device-info">
+ <tbody>
+ <if expr="not is_android">
+ <tr>
+ <td>
+ Android Id
+ </td>
+ <td>
+ <span id="android-id"></span>
+ <span id="android-secret-container" class="invisible">
+ (<b>secret</b>: <span id="android-secret"></span>)
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ User Profile Service Created
+ </td>
+ <td id="profile-service-created">
+ </td>
+ </tr>
+ </if>
+ <tr>
+ <td>
+ GCM Enabled
+ </td>
+ <td id="gcm-enabled">
+ </td>
+ </tr>
+ <if expr="not is_android">
+ <tr>
+ <td>
+ GCM Client Created
+ </td>
+ <td id="gcm-client-created">
+ </td>
+ </tr>
+ <tr>
+ <td>
+ GCM Client State
+ </td>
+ <td id="gcm-client-state">
+ </td>
+ </tr>
+ <tr>
+ <td>
+ Connection Client Created
+ </td>
+ <td id="connection-client-created">
+ </td>
+ </tr>
+ <tr>
+ <td>
+ Connection State
+ </td>
+ <td id="connection-state">
+ </td>
+ </tr>
+ <tr>
+ <td>
+ Last Checkin
+ </td>
+ <td id="last-checkin">
+ </td>
+ </tr>
+ <tr>
+ <td>
+ Next Checkin
+ </td>
+ <td id="next-checkin">
+ </td>
+ </tr>
+ <tr>
+ <td>
+ Registered App Ids
+ </td>
+ <td id="registered-app-ids">
+ </td>
+ </tr>
+ <tr>
+ <td>
+ Send Message Queue Size
+ </td>
+ <td id="send-queue-size">
+ </td>
+ </tr>
+ <tr>
+ <td>
+ Resend Message Queue Size
+ </td>
+ <td id="resend-queue-size">
+ </td>
+ </tr>
+ </if>
+ </tbody>
+</table>
+
+<if expr="not is_android">
+ <h2>Check-in Log</h2>
+ <table class="log-table">
+ <thead>
+ <tr>
+ <th>Time</th>
+ <th>Event</th>
+ <th>Details</th>
+ </tr>
+ </thead>
+ <tbody id="checkin-info">
+ </tbody>
+ </table>
+
+ <h2>Connection Log</h2>
+ <table class="log-table">
+ <thead>
+ <tr>
+ <th>Time</th>
+ <th>Event</th>
+ <th>Details</th>
+ </tr>
+ </thead>
+ <tbody id="connection-info">
+ </tbody>
+ </table>
+</if>
+
+<h2>Registration Log</h2>
+<table class="log-table">
+ <thead>
+ <tr>
+ <th>Time</th>
+ <th>App Id</th>
+ <th>Source</th>
+ <th>Event</th>
+ <th>Details</th>
+ </tr>
+ </thead>
+ <tbody id="registration-info">
+ </tbody>
+</table>
+
+<h2>Receive Message Log</h2>
+<table class="log-table">
+ <thead>
+ <tr>
+ <th>Time</th>
+ <th>App Id</th>
+ <th>From</th>
+ <th>Size (bytes)</th>
+ <th>Event</th>
+ <th>Details</th>
+ </tr>
+ </thead>
+ <tbody id="receive-info">
+ </tbody>
+</table>
+
+<h2>Message Decryption Failure Log</h2>
+<table class="log-table">
+ <thead>
+ <tr>
+ <th>Time</th>
+ <th>App Id</th>
+ <th>Details</th>
+ </tr>
+ </thead>
+ <tbody id="decryption-failure-info">
+ </tbody>
+</table>
+
+<if expr="not is_android">
+ <h2>Send Message Log</h2>
+ <table class="log-table">
+ <thead>
+ <tr>
+ <th>Time</th>
+ <th>App Id</th>
+ <th>Receiver Id</th>
+ <th>Msg Id</th>
+ <th>Event</th>
+ <th>Details</th>
+ </tr>
+ </thead>
+ <tbody id="send-info">
+ </tbody>
+ </table>
+</if>
+
+</body>
+</html>
diff --git a/chromium/components/gcm_driver/resources/gcm_internals.js b/chromium/components/gcm_driver/resources/gcm_internals.js
new file mode 100644
index 00000000000..32fe6a0ef51
--- /dev/null
+++ b/chromium/components/gcm_driver/resources/gcm_internals.js
@@ -0,0 +1,222 @@
+// Copyright 2014 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.
+
+// <if expr="is_ios">
+import 'chrome://resources/js/ios/web_ui.js';
+// </if>
+
+import './strings.m.js';
+import {addWebUIListener} from 'chrome://resources/js/cr.m.js';
+import {$} from 'chrome://resources/js/util.m.js';
+
+let isRecording = false;
+let keyPressState = 0;
+
+/**
+ * If the info dictionary has property prop, then set the text content of
+ * element to the value of this property. Otherwise clear the content.
+ * @param {!Object} info A dictionary of device infos to be displayed.
+ * @param {string} prop Name of the property.
+ * @param {string} elementId The id of a HTML element.
+ */
+function setIfExists(info, prop, elementId) {
+ const element = $(elementId);
+ if (!element) {
+ return;
+ }
+
+ if (info[prop] !== undefined) {
+ element.textContent = info[prop];
+ } else {
+ element.textContent = '';
+ }
+}
+
+/**
+ * Sets the registeredAppIds from |info| to the element identified by
+ * |elementId|. The list will have duplicates counted and visually shown.
+ * @param {!Object} info A dictionary of device infos to be displayed.
+ * @param {string} prop Name of the property.
+ * @param {string} elementId The id of a HTML element.
+ */
+function setRegisteredAppIdsIfExists(info, prop, elementId) {
+ const element = $(elementId);
+ if (!element) {
+ return;
+ }
+
+ if (info[prop] === undefined || !Array.isArray(info[prop])) {
+ return;
+ }
+
+ const registeredAppIds = new Map();
+ info[prop].forEach(registeredAppId => {
+ registeredAppIds.set(
+ registeredAppId, (registeredAppIds.get(registeredAppId) || 0) + 1);
+ });
+
+ const list = [];
+ for (const [registeredAppId, count] of registeredAppIds.entries()) {
+ list.push(registeredAppId + (count > 1 ? ` (x${count})` : ``));
+ }
+
+ element.textContent = list.join(', ');
+}
+
+/**
+ * Display device information.
+ * @param {!Object} info A dictionary of device infos to be displayed.
+ */
+function displayDeviceInfo(info) {
+ setIfExists(info, 'androidId', 'android-id');
+ setIfExists(info, 'androidSecret', 'android-secret');
+ setIfExists(info, 'profileServiceCreated', 'profile-service-created');
+ setIfExists(info, 'gcmEnabled', 'gcm-enabled');
+ setIfExists(info, 'gcmClientCreated', 'gcm-client-created');
+ setIfExists(info, 'gcmClientState', 'gcm-client-state');
+ setIfExists(info, 'connectionClientCreated', 'connection-client-created');
+ setIfExists(info, 'connectionState', 'connection-state');
+ setIfExists(info, 'lastCheckin', 'last-checkin');
+ setIfExists(info, 'nextCheckin', 'next-checkin');
+ setIfExists(info, 'sendQueueSize', 'send-queue-size');
+ setIfExists(info, 'resendQueueSize', 'resend-queue-size');
+
+ setRegisteredAppIdsIfExists(info, 'registeredAppIds', 'registered-app-ids');
+}
+
+/**
+ * Remove all the child nodes of the element.
+ * @param {HTMLElement} element A HTML element.
+ */
+function removeAllChildNodes(element) {
+ element.textContent = '';
+}
+
+/**
+ * For each item in line, add a row to the table. Each item is actually a list
+ * of sub-items; each of which will have a corresponding cell created in that
+ * row, and the sub-item will be displayed in the cell.
+ * @param {HTMLElement} table A HTML tbody element.
+ * @param {!Object} list A list of list of item.
+ */
+function addRows(table, list) {
+ for (let i = 0; i < list.length; ++i) {
+ const row = document.createElement('tr');
+
+ // The first element is always a timestamp.
+ let cell = document.createElement('td');
+ const d = new Date(list[i][0]);
+ cell.textContent = d;
+ row.appendChild(cell);
+
+ for (let j = 1; j < list[i].length; ++j) {
+ cell = document.createElement('td');
+ cell.textContent = list[i][j];
+ row.appendChild(cell);
+ }
+ table.appendChild(row);
+ }
+}
+
+/**
+ * Refresh all displayed information.
+ */
+function refreshAll() {
+ chrome.send('getGcmInternalsInfo', [false]);
+}
+
+/**
+ * Toggle the isRecording variable and send it to browser.
+ */
+function setRecording() {
+ isRecording = !isRecording;
+ chrome.send('setGcmInternalsRecording', [isRecording]);
+}
+
+/**
+ * Clear all the activity logs.
+ */
+function clearLogs() {
+ chrome.send('getGcmInternalsInfo', [true]);
+}
+
+function initialize() {
+ addWebUIListener('set-gcm-internals-info', setGcmInternalsInfo);
+ $('recording').disabled = true;
+ $('refresh').onclick = refreshAll;
+ $('recording').onclick = setRecording;
+ $('clear-logs').onclick = clearLogs;
+ chrome.send('getGcmInternalsInfo', [false]);
+
+ // Recording defaults to on.
+ chrome.send('setGcmInternalsRecording', [true]);
+}
+
+/**
+ * Allows displaying the Android Secret by typing a secret phrase.
+ *
+ * There are good reasons for displaying the Android Secret associated with
+ * the local connection info, but we also need to be careful to make sure that
+ * users don't share this value by accident. Therefore we require a secret
+ * phrase to be typed into the page for making it visible.
+ *
+ * @param {!Event} event The keypress event handler.
+ */
+function handleKeyPress(event) {
+ const PHRASE = 'secret';
+ if (PHRASE.charCodeAt(keyPressState) === event.keyCode) {
+ if (++keyPressState < PHRASE.length) {
+ return;
+ }
+
+ $('android-secret-container').classList.remove('invisible');
+ }
+
+ keyPressState = 0;
+}
+
+/**
+ * Refresh the log html table by clearing it first. If data is not empty, then
+ * it will be used to populate the table.
+ * @param {string} tableId ID of the log html table.
+ * @param {!Object} data A list of list of data items.
+ */
+function refreshLogTable(tableId, data) {
+ const element = $(tableId);
+ if (!element) {
+ return;
+ }
+
+ removeAllChildNodes(element);
+ if (data !== undefined) {
+ addRows(element, data);
+ }
+}
+
+/**
+ * Callback function accepting a dictionary of info items to be displayed.
+ * @param {!Object} infos A dictionary of info items to be displayed.
+ */
+function setGcmInternalsInfo(infos) {
+ isRecording = infos.isRecording;
+ if (isRecording) {
+ $('recording').textContent = 'Stop Recording';
+ } else {
+ $('recording').textContent = 'Start Recording';
+ }
+ $('recording').disabled = false;
+ if (infos.deviceInfo !== undefined) {
+ displayDeviceInfo(infos.deviceInfo);
+ }
+
+ refreshLogTable('checkin-info', infos.checkinInfo);
+ refreshLogTable('connection-info', infos.connectionInfo);
+ refreshLogTable('registration-info', infos.registrationInfo);
+ refreshLogTable('receive-info', infos.receiveInfo);
+ refreshLogTable('decryption-failure-info', infos.decryptionFailureInfo);
+ refreshLogTable('send-info', infos.sendInfo);
+}
+
+document.addEventListener('DOMContentLoaded', initialize);
+document.addEventListener('keypress', handleKeyPress);
diff --git a/chromium/components/gcm_driver/system_encryptor.cc b/chromium/components/gcm_driver/system_encryptor.cc
new file mode 100644
index 00000000000..bc731a30872
--- /dev/null
+++ b/chromium/components/gcm_driver/system_encryptor.cc
@@ -0,0 +1,23 @@
+// Copyright (c) 2014 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.
+
+#include "components/gcm_driver/system_encryptor.h"
+
+#include "components/os_crypt/os_crypt.h"
+
+namespace gcm {
+
+SystemEncryptor::~SystemEncryptor() {}
+
+bool SystemEncryptor::EncryptString(const std::string& plaintext,
+ std::string* ciphertext) {
+ return ::OSCrypt::EncryptString(plaintext, ciphertext);
+}
+
+bool SystemEncryptor::DecryptString(const std::string& ciphertext,
+ std::string* plaintext) {
+ return ::OSCrypt::DecryptString(ciphertext, plaintext);
+}
+
+} // namespace gcm
diff --git a/chromium/components/gcm_driver/system_encryptor.h b/chromium/components/gcm_driver/system_encryptor.h
new file mode 100644
index 00000000000..02339ad5559
--- /dev/null
+++ b/chromium/components/gcm_driver/system_encryptor.h
@@ -0,0 +1,27 @@
+// Copyright (c) 2014 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 COMPONENTS_GCM_DRIVER_SYSTEM_ENCRYPTOR_H_
+#define COMPONENTS_GCM_DRIVER_SYSTEM_ENCRYPTOR_H_
+
+#include "base/compiler_specific.h"
+#include "google_apis/gcm/base/encryptor.h"
+
+namespace gcm {
+
+// Encryptor that uses the Chrome password manager's encryptor.
+class SystemEncryptor : public Encryptor {
+ public:
+ ~SystemEncryptor() override;
+
+ bool EncryptString(const std::string& plaintext,
+ std::string* ciphertext) override;
+
+ bool DecryptString(const std::string& ciphertext,
+ std::string* plaintext) override;
+};
+
+} // namespace gcm
+
+#endif // COMPONENTS_GCM_DRIVER_SYSTEM_ENCRYPTOR_H_